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 = [
- '',
- '',
- '',
- _t('View as webpage'),
- ' ',
- '',
- _t('Edit'),
- ' ',
- '',
- _t('Delete'),
- ' ',
- ' ',
- ' '
- ].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: [
+ ''
+ ].join(''),
+ item: [
+ '',
+ '',
+ '',
+ _t('View as webpage'),
+ ' ',
+ '',
+ _t('Edit'),
+ '', _t('Note'), ' ',
+ ' ',
+ '',
+ _t('Delete'),
+ '', _t('Note'), ' ',
+ ' ',
+ '',
+ _t('Close'),
+ '', _t('Note'), ' ',
+ ' ',
+ ' ',
+ ' '
+ ].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('' + _t('No Comment') + ' ');
- 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 = [
+ ''
+ ].join('');
+
+ /**
+ * Modifies Annotator._setupEditor to add a label for textarea#annotator-field-0.
+ **/
+ Annotator.prototype._setupEditor = _.compose(
+ function () {
+ $('Edit note ').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;
}
});
});
diff --git a/lms/static/js/edxnotes/views/toggle_notes_factory.js b/lms/static/js/edxnotes/views/toggle_notes_factory.js
index 5739439a6b..7a37788dd2 100644
--- a/lms/static/js/edxnotes/views/toggle_notes_factory.js
+++ b/lms/static/js/edxnotes/views/toggle_notes_factory.js
@@ -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() {
diff --git a/lms/static/js/fixtures/edxnotes/toggle_notes.html b/lms/static/js/fixtures/edxnotes/toggle_notes.html
index 7b1d2775a9..7aa88bc965 100644
--- a/lms/static/js/fixtures/edxnotes/toggle_notes.html
+++ b/lms/static/js/fixtures/edxnotes/toggle_notes.html
@@ -1,5 +1,5 @@
- Hiding notes
+ Notes visible
Hide notes
diff --git a/lms/static/js/spec/edxnotes/plugins/accessibility_spec.js b/lms/static/js/spec/edxnotes/plugins/accessibility_spec.js
new file mode 100644
index 0000000000..f5d3090add
--- /dev/null
+++ b/lms/static/js/spec/edxnotes/plugins/accessibility_spec.js
@@ -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 = $(' ').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 = $(' ').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 = $(' ').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 = $(' ').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);
+ });
+ });
+ });
+});
diff --git a/lms/static/js/spec/edxnotes/views/shim_spec.js b/lms/static/js/spec/edxnotes/views/shim_spec.js
index 2c9e1de269..c4197496c4 100644
--- a/lms/static/js/spec/edxnotes/views/shim_spec.js
+++ b/lms/static/js/spec/edxnotes/views/shim_spec.js
@@ -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;
diff --git a/lms/static/js/spec/edxnotes/views/toggle_notes_factory_spec.js b/lms/static/js/spec/edxnotes/views/toggle_notes_factory_spec.js
index 29f1eb6c79..a310658186 100644
--- a/lms/static/js/spec/edxnotes/views/toggle_notes_factory_spec.js
+++ b/lms/static/js/spec/edxnotes/views/toggle_notes_factory_spec.js
@@ -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();
+ });
});
});
diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js
index 1911feca4a..177e71df4c 100644
--- a/lms/static/js/spec/main.js
+++ b/lms/static/js/spec/main.js
@@ -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',
diff --git a/lms/static/sass/course/modules/_student-notes.scss b/lms/static/sass/course/modules/_student-notes.scss
index 8aad051aa4..737e4dff01 100644
--- a/lms/static/sass/course/modules/_student-notes.scss
+++ b/lms/static/sass/course/modules/_student-notes.scss
@@ -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: "\f00d";
+ content: "\f1f8"; // .fa-trash
+ }
+ }
+
+ .annotator-close {
+
+ &:before {
+ top: -($baseline/20);
+ @include left(0);
+ 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;
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index b11ffb1d54..b80439ef4e 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -243,9 +243,7 @@ ${fragment.foot_html()}
-
- ${_('Course Utilities Navigation')}
-
+
## Utility: Chat
% if show_chat:
<%include file="/chat/toggle_chat.html" />
diff --git a/lms/templates/edxnotes/toggle_notes.html b/lms/templates/edxnotes/toggle_notes.html
index e50c6550e8..449a757c5f 100644
--- a/lms/templates/edxnotes/toggle_notes.html
+++ b/lms/templates/edxnotes/toggle_notes.html
@@ -9,7 +9,7 @@
%>
-
+
% if edxnotes_visibility:
${_("Hide notes")}