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:
......
;(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);
......@@ -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>
......
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);
});
});
});
});
......@@ -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',
......
......@@ -3,6 +3,7 @@
// in this document:
// --------------------
// +extends
// +notes
// +local variables/utilities
// +toggling notes
......@@ -11,6 +12,22 @@
// +listing notes
// +necessary, but ugly overrides
// +extends:
// --------------------
%bubble {
@include transform(rotate(45deg));
@include left(12px);
position: absolute;
bottom: -($baseline/2);
display: block;
width: 16px;
height: 16px;
content: " ";
background: $white; // Set a default
border-bottom: 1px solid $gray-l2;
border-right: 1px solid $gray-l2;
}
// +notes:
// --------------------
// this Sass partial contains all of the styling needed for the in-line student notes UI.
......@@ -101,6 +118,10 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
border-color: $error-color;
}
.edx-notes-focus-grabber {
outline: none;
}
.edx-notes-wrapper {
// +individual note (in context)
// --------------------
......@@ -117,6 +138,21 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
.annotator-listing {
padding: 0 !important;
margin: 0 !important;
.annotator-widget {
&:after {
@extend %bubble;
}
}
}
.annotator-editor {
.annotator-widget {
&:after {
@extend %bubble;
background: $gray-l5;
}
}
}
.annotator-item {
......@@ -148,15 +184,9 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
// using annotatorJS triangle styling for adder
&:before {
position: absolute;
@include left(8px);
bottom: -($baseline/2);
display: block;
width: 18px;
height: ($baseline/2);
content: "";
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAEiCAYAAAD0w4JOAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDY0MTMzNTM2QUQzMTFFMUE2REJERDgwQTM3Njg5NTUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDY0MTMzNTQ2QUQzMTFFMUE2REJERDgwQTM3Njg5NTUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2ODkwQjlFQzZBRDExMUUxQTZEQkREODBBMzc2ODk1NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpENjQxMzM1MjZBRDMxMUUxQTZEQkREODBBMzc2ODk1NSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkijPpwAABBRSURBVHja7JsJVBRX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);
background-position: 0 0;
@extend %bubble;
@include left(10px);
background: whitesmoke;
}
}
}
......@@ -167,20 +197,22 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
.annotator-controls {
@include text-align(left);
@include clearfix();
background: $notes-annotator-background-med !important; //matches annotator JS editing bubble triangle color
font-family: $f-sans-serif !important;
@extend %ui-depth1;
position: relative;
padding: 8px;
border: none !important;
border-radius: 0 !important;
background: $gray-l5 !important;
font-family: $f-sans-serif !important;
// actions
.annotator-save, .annotator-cancel {
@extend %notes-reset-background;
font-family: $f-sans-serif !important;
font-size: 14px !important;
padding: ($baseline/4) ($baseline/2) !important;
border: none;
box-shadow: none;
font-family: $f-sans-serif !important;
font-size: 14px !important;
text-shadow: none !important;
// removing vendor icons
......@@ -255,6 +287,10 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
// content
.annotator-viewer {
.annotator-widget.annotator-listing {
outline: none;
}
// poorly scoped selector for content of a note's comment
div:first-of-type {
@extend %notes-reset-font;
......@@ -266,13 +302,14 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
// controls
.annotator-controls {
opacity: 1;
// RTL support
@include right(0);
top: 0;
@include float(right);
@include padding-left($baseline/4);
.annotator-delete, .annotator-edit {
.annotator-edit, .annotator-delete, .annotator-close {
position: relative;
display: inline-block;
vertical-align: middle;
......@@ -291,16 +328,26 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
&:before {
top: 0;
@include left(0);
content: "\f044";
content: "\f040"; // .fa-pencil
}
}
.annotator-delete {
@include margin-right($baseline/3);
&:before {
top: -($baseline/20);
@include left(0);
content: "\f1f8"; // .fa-trash
}
}
.annotator-close {
&:before {
top: -($baseline/20);
@include left(0);
content: "\f00d";
content: "\f00d"; // .fa-close
}
}
}
......@@ -313,12 +360,12 @@ $notes-annotator-background-dark: rgba(122,122,122,0.6); // taken from annotator
opacity: 1.0;
}
.edx-notes-wrapper .annotator-wrapper .annotator-editor.annotator-outer a.annotator-save {
.edx-notes-wrapper .annotator-wrapper .annotator-editor.annotator-outer button.annotator-save {
@extend %btn-inherited-primary;
@extend %t-action2;
}
.edx-notes-wrapper .annotator-wrapper .annotator-editor.annotator-outer a.annotator-cancel {
.edx-notes-wrapper .annotator-wrapper .annotator-editor.annotator-outer button.annotator-cancel {
@extend %shame-link-base;
@extend %t-action2;
@extend %t-regular;
......
......@@ -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