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

TNL 713: Added accessibility plugin.
This commit is contained in:
Anton Stupak
2015-02-20 14:33:04 +02:00
14 changed files with 862 additions and 71 deletions

View File

@@ -1,4 +1,5 @@
<%! import json %>
<%! from django.utils.translation import ugettext as _ %>
<%! from student.models import anonymous_id_for_user %>
<%
if user:

View File

@@ -0,0 +1,320 @@
;(function (define, undefined) {
'use strict';
define(['jquery', 'underscore', 'annotator_1.2.9'], function ($, _, Annotator) {
/**
* Adds the Accessibility Plugin
**/
Annotator.Plugin.Accessibility = function () {
_.bindAll(this,
'addAriaAttributes', 'onHighlightKeyDown', 'onViewerKeyDown',
'onEditorKeyDown', 'addDescriptions', 'removeDescription',
'saveCurrentHighlight', 'focusOnGrabber', 'showViewer', 'onClose'
);
// Call the Annotator.Plugin constructor this sets up the element and
// options properties.
Annotator.Plugin.apply(this, arguments);
};
$.extend(Annotator.Plugin.Accessibility.prototype, new Annotator.Plugin(), {
pluginInit: function () {
this.annotator.subscribe('annotationViewerTextField', this.addAriaAttributes);
this.annotator.subscribe('annotationsLoaded', this.addDescriptions);
this.annotator.subscribe('annotationCreated', this.addDescriptions);
this.annotator.subscribe('annotationDeleted', this.removeDescription);
this.annotator.element.on('keydown.accessibility.hl', '.annotator-hl', this.onHighlightKeyDown);
this.annotator.element.on('keydown.accessibility.viewer', '.annotator-viewer', this.onViewerKeyDown);
this.annotator.element.on('keydown.accessibility.editor', '.annotator-editor', this.onEditorKeyDown);
this.addFocusGrabber();
this.addTabIndex();
},
destroy: function () {
this.annotator.unsubscribe('annotationViewerTextField', this.addAriaAttributes);
this.annotator.unsubscribe('annotationsLoaded', this.addDescriptions);
this.annotator.unsubscribe('annotationCreated', this.addDescriptions);
this.annotator.unsubscribe('annotationDeleted', this.removeDescription);
this.annotator.element.off('.accessibility');
this.removeFocusGrabber();
this.savedHighlights = null;
},
addTabIndex: function () {
this.annotator.element
.find('.annotator-edit, .annotator-delete')
.attr('tabindex', 0);
},
addFocusGrabber: function () {
this.focusGrabber = $('<span />', {
'class': 'sr edx-notes-focus-grabber',
'tabindex': '-1',
'text': gettext('Focus grabber')
});
this.annotator.wrapper.before(this.focusGrabber);
},
removeFocusGrabber: function () {
if (this.focusGrabber) {
this.focusGrabber.remove();
this.focusGrabber = null;
}
},
focusOnGrabber: function () {
this.annotator.wrapper.siblings('.edx-notes-focus-grabber').focus();
},
addDescriptions: function (annotations) {
if (!_.isArray(annotations)) {
annotations = [annotations];
}
_.each(annotations, function (annotation) {
var id = annotation.id || _.uniqueId();
this.annotator.wrapper.after($('<div />', {
'class': 'aria-note-description sr',
'id': 'aria-note-description-' + id,
'text': Annotator.Util.escape(annotation.text)
}));
$(annotation.highlights).attr({
'aria-describedby': 'aria-note-description-' + id
});
}, this);
},
removeDescription: function (annotation) {
var id = $(annotation.highlights).attr('aria-describedby');
$('#' + id).remove();
},
addAriaAttributes: function (field, annotation) {
// Add ARIA attributes to associated note ie <div>My note</div>
$(field).attr({
'tabindex': -1,
'role': 'note',
'class': 'annotator-note'
});
},
saveCurrentHighlight: function (annotation) {
if (annotation && annotation.highlights) {
this.savedHighlights = annotation.highlights[0];
}
},
focusOnHighlightedText: function () {
if (this.savedHighlights) {
this.savedHighlights.focus();
this.savedHighlights = null;
}
},
getViewerTabControls: function () {
var viewer, note, viewerControls, editButton, delButton, closeButton, tabControls = [];
// Viewer elements
viewer = this.annotator.element.find('.annotator-viewer');
note = viewer.find('.annotator-note');
viewerControls = viewer.find('.annotator-controls');
editButton = viewerControls.find('.annotator-edit');
delButton = viewerControls.find('.annotator-delete');
closeButton = viewerControls.find('.annotator-close');
tabControls.push(note, editButton, delButton, closeButton);
return tabControls;
},
getEditorTabControls: function () {
var editor, editorControls, textArea, saveButton, cancelButton, tabControls = [];
// Editor elements
editor = this.annotator.element.find('.annotator-editor');
editorControls = editor.find('.annotator-controls');
textArea = editor.find('.annotator-listing')
.find('.annotator-item')
.first()
.children('textarea');
saveButton = editorControls.find('.annotator-save');
cancelButton = editorControls.find('.annotator-cancel');
tabControls.push(textArea, saveButton, cancelButton);
return tabControls;
},
focusOnNextTabControl: function (tabControls, tabControl) {
var nextIndex;
_.each(tabControls, function (element, index) {
if (element.is(tabControl)) {
nextIndex = index === tabControls.length - 1 ? 0 : index + 1;
tabControls[nextIndex].focus();
}
});
},
focusOnPreviousTabControl: function (tabControls, tabControl) {
var previousIndex;
_.each(tabControls, function (element, index) {
if (element.is(tabControl)) {
previousIndex = index === 0 ? tabControls.length - 1 : index - 1;
tabControls[previousIndex].focus();
}
});
},
showViewer: function (position, annotation) {
annotation = $.makeArray(annotation);
this.saveCurrentHighlight(annotation[0]);
this.annotator.showViewer(annotation, position);
this.annotator.element.find('.annotator-listing').focus();
this.annotator.subscribe('annotationDeleted', this.focusOnGrabber);
},
onClose: function () {
this.focusOnHighlightedText();
this.annotator.unsubscribe('annotationDeleted', this.focusOnGrabber);
},
onHighlightKeyDown: function (event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.currentTarget),
annotation, position;
switch (keyCode) {
case KEY.TAB:
// This happens only when coming from notes page
if (this.annotator.viewer.isShown()) {
this.annotator.element.find('.annotator-listing').focus();
}
break;
case KEY.ENTER:
case KEY.SPACE:
if (!this.annotator.viewer.isShown()) {
position = target.position();
this.showViewer(position, target.data('annotation'));
}
break;
case KEY.ESCAPE:
this.annotator.viewer.hide();
break;
}
// We do not stop propagation and default behavior on a TAB keypress
if (event.keyCode !== KEY.TAB || (event.keyCode === KEY.TAB && this.annotator.viewer.isShown())) {
event.preventDefault();
event.stopPropagation();
}
},
onViewerKeyDown: function (event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.target),
listing = this.annotator.element.find('.annotator-listing'),
tabControls;
switch (keyCode) {
case KEY.TAB:
tabControls = this.getViewerTabControls();
if (event.shiftKey) { // Tabbing backwards
if (target.is(listing)) {
_.last(tabControls).focus();
}
else {
this.focusOnPreviousTabControl(tabControls, target);
}
} else { // Tabbing forward
if (target.is(listing)) {
_.first(tabControls).focus();
}
else {
this.focusOnNextTabControl(tabControls, target);
}
}
event.preventDefault();
event.stopPropagation();
break;
case KEY.ENTER:
case KEY.SPACE:
if (target.hasClass('annotator-close')) {
this.annotator.viewer.hide();
this.onClose();
event.preventDefault();
}
break;
case KEY.ESCAPE:
this.annotator.viewer.hide();
this.onClose();
event.preventDefault();
break;
}
},
onEditorKeyDown: function (event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.target),
editor, form, editorControls, save, cancel,
tabControls;
editor = this.annotator.element.find('.annotator-editor');
form = editor.find('.annotator-widget');
editorControls = editor.find('.annotator-controls');
save = editorControls.find('.annotator-save');
cancel = editorControls.find('.annotator-cancel');
switch (keyCode) {
case KEY.TAB:
tabControls = this.getEditorTabControls();
if (event.shiftKey) { // Tabbing backwards
if (target.is(form)) {
_.last(tabControls).focus();
} else {
this.focusOnPreviousTabControl(tabControls, target);
}
} else { // Tabbing forward
if (target.is(form)) {
_.first(tabControls).focus();
} else {
this.focusOnNextTabControl(tabControls, target);
}
}
event.preventDefault();
event.stopPropagation();
break;
case KEY.ENTER:
if (target.is(save) || event.metaKey || event.ctrlKey) {
this.annotator.editor.submit();
} else if (target.is(cancel)) {
this.annotator.editor.hide();
} else {
break;
}
this.onClose();
event.preventDefault();
break;
case KEY.SPACE:
if (target.is(save)) {
this.annotator.editor.submit();
} else if (target.is(cancel)) {
this.annotator.editor.hide();
} else {
break;
}
this.onClose();
event.preventDefault();
break;
case KEY.ESCAPE:
this.annotator.editor.hide();
this.onClose();
event.preventDefault();
break;
}
}
});
});
}).call(this, define || RequireJS.define);

View File

@@ -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);
}

View File

@@ -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;
/**

View File

@@ -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({
@@ -147,6 +164,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;
}
});
});

View File

@@ -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() {

View File

@@ -1,5 +1,5 @@
<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>

View File

@@ -0,0 +1,318 @@
define([
'jquery', 'underscore', 'annotator_1.2.9', 'logger', 'js/edxnotes/views/notes_factory', 'js/spec/edxnotes/custom_matchers'
], function($, _, Annotator, Logger, NotesFactory, customMatchers) {
'use strict';
describe('EdxNotes Accessibility Plugin', function() {
function keyDownEvent (key) {
return $.Event('keydown', {keyCode: key});
}
function tabBackwardEvent () {
return $.Event('keydown', {keyCode: $.ui.keyCode.TAB, shiftKey: true});
}
function tabForwardEvent () {
return $.Event('keydown', {keyCode: $.ui.keyCode.TAB, shiftKey: false});
}
function enterMetaKeyEvent () {
return $.Event('keydown', {keyCode: $.ui.keyCode.ENTER, metaKey: true});
}
function enterControlKeyEvent () {
return $.Event('keydown', {keyCode: $.ui.keyCode.ENTER, ctrlKey: true});
}
beforeEach(function() {
this.KEY = $.ui.keyCode;
customMatchers(this);
loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html');
this.annotator = NotesFactory.factory(
$('div#edx-notes-wrapper-123').get(0), {
endpoint: 'http://example.com/'
}
);
this.plugin = this.annotator.plugins.Accessibility;
spyOn(Logger, 'log');
});
afterEach(function () {
_.invoke(Annotator._instances, 'destroy');
});
describe('destroy', function () {
it('should unbind all events', function () {
spyOn($.fn, 'off');
spyOn(this.annotator, 'unsubscribe');
this.plugin.destroy();
expect(this.annotator.unsubscribe).toHaveBeenCalledWith(
'annotationViewerTextField', this.plugin.addAriaAttributes
);
expect(this.annotator.unsubscribe).toHaveBeenCalledWith(
'annotationsLoaded', this.plugin.addDescriptions
);
expect(this.annotator.unsubscribe).toHaveBeenCalledWith(
'annotationCreated', this.plugin.addDescriptions
);
expect(this.annotator.unsubscribe).toHaveBeenCalledWith(
'annotationDeleted', this.plugin.removeDescription
);
expect($.fn.off).toHaveBeenCalledWith('.accessibility');
});
});
describe('a11y attributes', function () {
var highlight, annotation, note;
beforeEach(function() {
highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element);
annotation = {
id: '01',
text: 'Test text',
highlights: [highlight.get(0)]
};
});
it('should be added to highlighted text and associated note', function () {
this.annotator.viewer.load([annotation]);
note = $('.annotator-note');
expect(note).toExist();
expect(note).toHaveAttr('tabindex', -1);
expect(note).toHaveAttr('role', 'note');
expect(note).toHaveAttr('class', 'annotator-note');
});
it('should create aria-descriptions when annotations are loaded', function () {
this.annotator.publish('annotationsLoaded', [[annotation]]);
expect(highlight).toHaveAttr('aria-describedby', 'aria-note-description-01');
expect($('#aria-note-description-01')).toContainText('Test text');
});
it('should create aria-description when new annotation is created', function () {
this.annotator.publish('annotationCreated', [annotation]);
expect(highlight).toHaveAttr('aria-describedby', 'aria-note-description-01');
expect($('#aria-note-description-01')).toContainText('Test text');
});
it('should remove aria-description when the annotation is removed', function () {
this.annotator.publish('annotationDeleted', [annotation]);
expect($('#aria-note-description-01')).not.toExist();
});
});
describe('keydown events on highlighted text', function () {
var highlight, annotation, note;
beforeEach(function() {
highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element);
annotation = {
id: '01',
text: 'Test text',
highlights: [highlight.get(0)]
};
highlight.data('annotation', annotation);
spyOn(this.annotator, 'showViewer').andCallThrough();
spyOn(this.annotator.viewer, 'hide').andCallThrough();
spyOn(this.plugin, 'focusOnGrabber').andCallThrough();
});
it('should open the viewer on SPACE keydown and focus on note', function () {
highlight.trigger(keyDownEvent(this.KEY.SPACE));
expect(this.annotator.showViewer).toHaveBeenCalled();
});
it('should open the viewer on ENTER keydown and focus on note', function () {
highlight.trigger(keyDownEvent(this.KEY.ENTER));
expect(this.annotator.showViewer).toHaveBeenCalled();
});
// This happens only when coming from notes page
it('should open focus on viewer on TAB keydown if viewer is opened', function () {
this.annotator.viewer.load([annotation]);
highlight.trigger(keyDownEvent(this.KEY.TAB));
expect(this.annotator.element.find('.annotator-listing')).toBeFocused();
});
it('should focus highlighted text after closing', function () {
var note;
highlight.trigger(keyDownEvent(this.KEY.ENTER));
expect(this.plugin.savedHighlights).toBeDefined();
note = this.annotator.element.find('.annotator-edit');
note.trigger(keyDownEvent(this.KEY.ESCAPE));
expect(highlight).toBeFocused();
expect(this.plugin.savedHighlights).toBeNull();
});
it('should focus on grabber after being deleted', function () {
highlight.trigger(keyDownEvent(this.KEY.ENTER));
this.annotator.publish('annotationDeleted', {});
expect(this.plugin.focusGrabber).toBeFocused();
});
it('should not focus on grabber when the viewer is hidden', function () {
this.annotator.publish('annotationDeleted', {});
expect(this.plugin.focusGrabber).not.toBeFocused();
});
});
describe('keydown events on viewer', function () {
var highlight, annotation, listing, note, edit, del, close;
beforeEach(function() {
highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element);
annotation = {
id: '01',
text: "Test text",
highlights: [highlight.get(0)]
};
highlight.data('annotation', annotation);
this.annotator.viewer.load([annotation]);
listing = this.annotator.element.find('.annotator-listing').first(),
note = this.annotator.element.find('.annotator-note').first();
edit= this.annotator.element.find('.annotator-edit').first();
del = this.annotator.element.find('.annotator-delete').first();
close = this.annotator.element.find('.annotator-close').first();
spyOn(this.annotator.viewer, 'hide').andCallThrough();;
});
it('should give focus to Note on Listing TAB keydown', function () {
listing.focus();
listing.trigger(tabForwardEvent());
expect(note).toBeFocused();
});
it('should give focus to Close on Listing SHIFT + TAB keydown', function () {
listing.focus();
listing.trigger(tabBackwardEvent());
expect(close).toBeFocused();
});
it('should cycle forward through Note, Edit, Delete, and Close on TAB keydown', function () {
note.focus();
note.trigger(tabForwardEvent());
expect(edit).toBeFocused();
edit.trigger(tabForwardEvent());
expect(del).toBeFocused();
del.trigger(tabForwardEvent());
expect(close).toBeFocused();
close.trigger(tabForwardEvent());
expect(note).toBeFocused();
});
it('should cycle backward through Note, Edit, Delete, and Close on SHIFT + TAB keydown', function () {
note.focus();
note.trigger(tabBackwardEvent());
expect(close).toBeFocused();
close.trigger(tabBackwardEvent());
expect(del).toBeFocused();
del.trigger(tabBackwardEvent());
expect(edit).toBeFocused();
edit.trigger(tabBackwardEvent());
expect(note).toBeFocused();
});
it('should hide on ESCAPE keydown', function () {
var tabControls = [listing, note, edit, del, close];
_.each(tabControls, function (control) {
control.focus();
control.trigger(keyDownEvent(this.KEY.ESCAPE));
}, this);
expect(this.annotator.viewer.hide.callCount).toBe(5);
});
});
describe('keydown events on editor', function () {
var highlight, annotation, form, textArea, save, cancel;
beforeEach(function() {
highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element);
annotation = {
id: '01',
text: "Test text",
highlights: [highlight.get(0)]
};
highlight.data('annotation', annotation);
this.annotator.editor.show(annotation, {'left': 0, 'top': 0});
form = this.annotator.element.find('form.annotator-widget');
textArea = this.annotator.element.find('.annotator-item').first().children('textarea');
save = this.annotator.element.find('.annotator-save');
cancel = this.annotator.element.find('.annotator-cancel');
spyOn(this.annotator.editor, 'submit').andCallThrough();
spyOn(this.annotator.editor, 'hide').andCallThrough();
});
it('should give focus to TextArea on Form TAB keydown', function () {
form.focus();
form.trigger(tabForwardEvent());
expect(textArea).toBeFocused();
});
it('should give focus to Cancel on Form SHIFT + TAB keydown', function () {
form.focus();
form.trigger(tabBackwardEvent());
expect(cancel).toBeFocused();
});
it('should cycle forward through texarea, save, and cancel on TAB keydown', function () {
textArea.focus();
textArea.trigger(tabForwardEvent());
expect(save).toBeFocused();
save.trigger(tabForwardEvent());
expect(cancel).toBeFocused();
cancel.trigger(tabForwardEvent());
expect(textArea).toBeFocused();
});
it('should cycle back through texarea, save, and cancel on SHIFT + TAB keydown', function () {
textArea.focus();
textArea.trigger(tabBackwardEvent());
expect(cancel).toBeFocused();
cancel.trigger(tabBackwardEvent());
expect(save).toBeFocused();
save.trigger(tabBackwardEvent());
expect(textArea).toBeFocused();
});
it('should submit if target is Save on ENTER or SPACE keydown', function () {
save.focus();
save.trigger(keyDownEvent(this.KEY.ENTER));
expect(this.annotator.editor.submit).toHaveBeenCalled();
this.annotator.editor.submit.reset();
save.focus();
save.trigger(keyDownEvent(this.KEY.SPACE));
expect(this.annotator.editor.submit).toHaveBeenCalled();
});
it('should submit on META or CONTROL + ENTER keydown', function () {
textArea.focus();
textArea.trigger(enterMetaKeyEvent());
expect(this.annotator.editor.submit).toHaveBeenCalled();
this.annotator.editor.submit.reset();
textArea.focus();
textArea.trigger(enterControlKeyEvent());
expect(this.annotator.editor.submit).toHaveBeenCalled();
});
it('should hide if target is Cancel on ENTER or SPACE keydown', function () {
cancel.focus();
cancel.trigger(keyDownEvent(this.KEY.ENTER));
expect(this.annotator.editor.hide).toHaveBeenCalled();
this.annotator.editor.hide.reset();
cancel.focus();
save.trigger(keyDownEvent(this.KEY.SPACE));
expect(this.annotator.editor.hide).toHaveBeenCalled();
});
it('should hide on ESCAPE keydown', function () {
var tabControls = [textArea, save, cancel];
_.each(tabControls, function (control) {
control.focus();
control.trigger(keyDownEvent(this.KEY.ESCAPE));
}, this);
expect(this.annotator.editor.hide.callCount).toBe(3);
});
});
});
});

View File

@@ -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;

View File

@@ -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();
});
});
});

View File

@@ -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',

File diff suppressed because one or more lines are too long

View File

@@ -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" />

View File

@@ -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>