diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9680759f8b..e2ebb76e53 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +Blades: Improve calculator's tooltip accessibility. Add possibility to navigate + through the hints via arrow keys. BLD-533. + LMS: Add feature for providing background grade report generation via Celery instructor task, with reports uploaded to S3. Feature is visible on the beta instructor dashboard. LMS-58 diff --git a/lms/static/coffee/fixtures/calculator.html b/lms/static/coffee/fixtures/calculator.html index 17d163eb67..638dcc0b1f 100644 --- a/lms/static/coffee/fixtures/calculator.html +++ b/lms/static/coffee/fixtures/calculator.html @@ -6,8 +6,11 @@
- - + Hints +
diff --git a/lms/static/coffee/spec/calculator_spec.coffee b/lms/static/coffee/spec/calculator_spec.coffee index 8e41ebcb3b..ed10bb26a6 100644 --- a/lms/static/coffee/spec/calculator_spec.coffee +++ b/lms/static/coffee/spec/calculator_spec.coffee @@ -1,4 +1,16 @@ describe 'Calculator', -> + + KEY = + TAB : 9 + ENTER : 13 + ALT : 18 + ESC : 27 + SPACE : 32 + LEFT : 37 + UP : 38 + RIGHT : 39 + DOWN : 40 + beforeEach -> loadFixtures 'coffee/fixtures/calculator.html' @calculator = new Calculator @@ -9,15 +21,14 @@ describe 'Calculator', -> it 'bind the help button', -> # These events are bind by $.hover() - expect($('div.help-wrapper a')).toHandle 'mouseover' - expect($('div.help-wrapper a')).toHandle 'mouseout' - expect($('div.help-wrapper')).toHandle 'focusin' - expect($('div.help-wrapper')).toHandle 'focusout' + expect($('#calculator_hint')).toHandle 'mouseover' + expect($('#calculator_hint')).toHandle 'mouseout' + expect($('#calculator_hint')).toHandle 'keydown' it 'prevent default behavior on help button', -> - $('div.help-wrapper a').click (e) -> + $('#calculator_hint').click (e) -> expect(e.isDefaultPrevented()).toBeTruthy() - $('div.help-wrapper a').click() + $('#calculator_hint').click() it 'bind the calculator submit', -> expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate @@ -51,30 +62,261 @@ describe 'Calculator', -> @calculator.toggle(jQuery.Event("click")) expect($('.calc')).not.toHaveClass('closed') - describe 'helpShow', -> + describe 'showHint', -> it 'show the help overlay', -> - @calculator.helpShow() + @calculator.showHint() expect($('.help')).toHaveClass('shown') expect($('.help')).toHaveAttr('aria-hidden', 'false') - describe 'helpHide', -> + + describe 'hideHint', -> it 'show the help overlay', -> - @calculator.helpHide() + @calculator.hideHint() expect($('.help')).not.toHaveClass('shown') expect($('.help')).toHaveAttr('aria-hidden', 'true') - describe 'handleKeyDown', -> - it 'on pressing Esc the hint becomes hidden', -> - @calculator.helpShow() - e = jQuery.Event('keydown', { which: 27 } ); + describe 'handleClickOnDocument', -> + it 'on click out of the hint popup it becomes hidden', -> + @calculator.showHint() + e = jQuery.Event('click'); $(document).trigger(e); expect($('.help')).not.toHaveClass 'shown' - it 'On pressing other buttons the hint continue to show', -> - @calculator.helpShow() - e = jQuery.Event('keydown', { which: 32 } ); - $(document).trigger(e); - expect($('.help')).toHaveClass 'shown' + describe 'selectHint', -> + it 'select correct hint item', -> + spyOn($.fn, 'focus') + element = $('.hint-item').eq(1) + @calculator.selectHint(element) + + expect(element.focus).toHaveBeenCalled() + expect(@calculator.activeHint).toEqual(element) + expect(@calculator.hintPopup).toHaveAttr('aria-activedescendant', element.attr('id')) + + it 'select the first hint if argument element is not passed', -> + @calculator.selectHint() + expect(@calculator.activeHint.attr('id')).toEqual($('.hint-item').first().attr('id')) + + it 'select the first hint if argument element is empty', -> + @calculator.selectHint([]) + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').first().attr('id')) + + describe 'prevHint', -> + + it 'Prev hint item is selected', -> + @calculator.activeHint = $('.hint-item').eq(1) + @calculator.prevHint() + + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id')) + + it 'Prev hint item is selected', -> + @calculator.activeHint = $('.hint-item').eq(1) + @calculator.prevHint() + + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id')) + + it 'if this was the first item, select the last one', -> + @calculator.activeHint = $('.hint-item').eq(0) + @calculator.prevHint() + + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id')) + + describe 'nextHint', -> + + it 'Next hint item is selected', -> + @calculator.activeHint = $('.hint-item').eq(0) + @calculator.nextHint() + + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(1).attr('id')) + + it 'If this was the last item, select the first one', -> + @calculator.activeHint = $('.hint-item').eq(1) + @calculator.nextHint() + + expect(@calculator.activeHint.attr('id')).toBe($('.hint-item').eq(0).attr('id')) + + describe 'handleKeyDown', -> + assertHintIsHidden = (calc, key) -> + spyOn(calc, 'hideHint') + calc.showHint() + e = jQuery.Event('keydown', { keyCode: key }); + value = calc.handleKeyDown(e) + + expect(calc.hideHint).toHaveBeenCalled + expect(value).toBeFalsy() + expect(e.isDefaultPrevented()).toBeTruthy() + + assertHintIsVisible = (calc, key) -> + spyOn(calc, 'showHint') + spyOn($.fn, 'focus') + e = jQuery.Event('keydown', { keyCode: key }); + value = calc.handleKeyDown(e) + + expect(calc.showHint).toHaveBeenCalled + expect(value).toBeFalsy() + expect(e.isDefaultPrevented()).toBeTruthy() + expect(calc.activeHint.focus).toHaveBeenCalled() + + assertNothingHappens = (calc, key) -> + spyOn(calc, 'showHint') + e = jQuery.Event('keydown', { keyCode: key }); + value = calc.handleKeyDown(e) + + expect(calc.showHint).not.toHaveBeenCalled + expect(value).toBeTruthy() + expect(e.isDefaultPrevented()).toBeFalsy() + + it 'hint popup becomes hidden on press ENTER', -> + assertHintIsHidden(@calculator, KEY.ENTER) + + it 'hint popup becomes visible on press ENTER', -> + assertHintIsVisible(@calculator, KEY.ENTER) + + it 'hint popup becomes hidden on press SPACE', -> + assertHintIsHidden(@calculator, KEY.SPACE) + + it 'hint popup becomes visible on press SPACE', -> + assertHintIsVisible(@calculator, KEY.SPACE) + + it 'Nothing happens on press ALT', -> + assertNothingHappens(@calculator, KEY.ALT) + + it 'Nothing happens on press any other button', -> + assertNothingHappens(@calculator, KEY.DOWN) + + describe 'handleKeyDownOnHint', -> + it 'Navigation works in proper way', -> + calc = @calculator + + eventToShowHint = jQuery.Event('keydown', { keyCode: KEY.ENTER } ); + $('#calculator_hint').trigger(eventToShowHint); + + spyOn(calc, 'hideHint') + spyOn(calc, 'prevHint') + spyOn(calc, 'nextHint') + spyOn($.fn, 'focus') + + cases = + left: + event: + keyCode: KEY.LEFT + shiftKey: false + returnedValue: false + called: + 'prevHint': calc + isPropagationStopped: true + + leftWithShift: + returnedValue: true + event: + keyCode: KEY.LEFT + shiftKey: true + not_called: + 'prevHint': calc + + up: + event: + keyCode: KEY.UP + shiftKey: false + returnedValue: false + called: + 'prevHint': calc + isPropagationStopped: true + + upWithShift: + returnedValue: true + event: + keyCode: KEY.UP + shiftKey: true + not_called: + 'prevHint': calc + + right: + event: + keyCode: KEY.RIGHT + shiftKey: false + returnedValue: false + called: + 'nextHint': calc + isPropagationStopped: true + + rightWithShift: + returnedValue: true + event: + keyCode: KEY.RIGHT + shiftKey: true + not_called: + 'nextHint': calc + + down: + event: + keyCode: KEY.DOWN + shiftKey: false + returnedValue: false + called: + 'nextHint': calc + isPropagationStopped: true + + downWithShift: + returnedValue: true + event: + keyCode: KEY.DOWN + shiftKey: true + not_called: + 'nextHint': calc + + tab: + returnedValue: true + event: + keyCode: KEY.TAB + shiftKey: false + called: + 'hideHint': calc + + esc: + returnedValue: false + event: + keyCode: KEY.ESC + shiftKey: false + called: + 'hideHint': calc + 'focus': $.fn + isPropagationStopped: true + + alt: + returnedValue: true + event: + which: KEY.ALT + not_called: + 'hideHint': calc + 'nextHint': calc + 'prevHint': calc + + $.each(cases, (key, data) -> + calc.hideHint.reset() + calc.prevHint.reset() + calc.nextHint.reset() + $.fn.focus.reset() + + e = jQuery.Event('keydown', data.event or {}); + value = calc.handleKeyDownOnHint(e) + + if data.called + $.each(data.called, (method, obj) -> + expect(obj[method]).toHaveBeenCalled() + ) + + if data.not_called + $.each(data.not_called, (method, obj) -> + expect(obj[method]).not.toHaveBeenCalled() + ) + + if data.isPropagationStopped + expect(e.isPropagationStopped()).toBeTruthy() + else + expect(e.isPropagationStopped()).toBeFalsy() + + expect(value).toBe(data.returnedValue) + ) describe 'calculate', -> beforeEach -> diff --git a/lms/static/coffee/src/calculator.coffee b/lms/static/coffee/src/calculator.coffee index c54a235581..230ff5e922 100644 --- a/lms/static/coffee/src/calculator.coffee +++ b/lms/static/coffee/src/calculator.coffee @@ -1,21 +1,48 @@ +# Keyboard Support + +# If focus is on the hint button: +# * Enter: Open or close hint popup. Select last focused hint item if opening +# * Space: Open or close hint popup. Select last focused hint item if opening + +# If focus is on a hint item: +# * Left arrow: Select previous hint item +# * Up arrow: Select previous hint item +# * Right arrow: Select next hint item +# * Down arrow: Select next hint item + + class @Calculator constructor: -> + @hintButton = $('#calculator_hint') + @hintPopup = $('.help') + @hintsList = @hintPopup.find('.hint-item') + @selectHint($('#' + @hintPopup.attr('aria-activedescendant'))); + $('.calc').click @toggle $('form#calculator').submit(@calculate).submit (e) -> e.preventDefault() - $('div.help-wrapper a') + @hintButton .hover( - $.proxy(@helpShow, @), - $.proxy(@helpHide, @) + $.proxy(@showHint, @), + $.proxy(@hideHint, @) ) - .click (e) -> - e.preventDefault() + .keydown($.proxy(@handleKeyDown, @)) + .click (e) -> e.preventDefault() - $(document).keydown $.proxy(@handleKeyDown, @) + @hintPopup + .keydown($.proxy(@handleKeyDownOnHint, @)) - $('div.help-wrapper') - .focusin($.proxy @helpOnFocus, @) - .focusout($.proxy @helpOnBlur, @) + @handleClickOnDocument = $.proxy(@handleClickOnDocument, @) + + KEY: + TAB : 9 + ENTER : 13 + ESC : 27 + SPACE : 32 + LEFT : 37 + UP : 38 + RIGHT : 39 + DOWN : 40 toggle: (event) -> event.preventDefault() @@ -49,32 +76,110 @@ class @Calculator $calc.toggleClass 'closed' - helpOnFocus: (e) -> - e.preventDefault() - @isFocusedHelp = true - @helpShow() - - helpOnBlur: (e) -> - e.preventDefault() - @isFocusedHelp = false - @helpHide() - - helpShow: -> - $('.help') + showHint: -> + @hintPopup .addClass('shown') .attr('aria-hidden', false) - helpHide: -> - if not @isFocusedHelp - $('.help') - .removeClass('shown') - .attr('aria-hidden', true) + $(document).on('click', @handleClickOnDocument) + + hideHint: -> + @hintPopup + .removeClass('shown') + .attr('aria-hidden', true) + + $(document).off('click', @handleClickOnDocument) + + selectHint: (element) -> + if not element or (element and element.length == 0) + element = @hintsList.first() + + @activeHint = element; + @activeHint.focus(); + @hintPopup.attr('aria-activedescendant', element.attr('id')); + + prevHint: () -> + prev = @activeHint.prev(); # the previous hint + # if this was the first item + # select the last one in the group. + if @activeHint.index() == 0 + prev = @hintsList.last() + # select the previous hint + @selectHint(prev) + + nextHint: () -> + next = @activeHint.next(); # the next hint + # if this was the last item, + # select the first one in the group. + if @activeHint.index() == @hintsList.length - 1 + next = @hintsList.first() + # give the next hint focus + @selectHint(next) handleKeyDown: (e) -> - ESC = 27 - if e.which is ESC and $('.help').hasClass 'shown' - @isFocusedHelp = false - @helpHide() + if e.altKey + # do nothing + return true + + if e.keyCode == @KEY.ENTER or e.keyCode == @KEY.SPACE + if @hintPopup.hasClass 'shown' + @hideHint() + else + @showHint() + @activeHint.focus() + + e.preventDefault() + return false + + # allow the event to propagate + return true + + handleKeyDownOnHint: (e) -> + if e.altKey + # do nothing + return true + + switch e.keyCode + when @KEY.TAB + # hide popup with hints + @hideHint() + + when @KEY.ESC + # hide popup with hints + @hideHint() + @hintButton.focus() + + e.stopPropagation() + return false + + when @KEY.LEFT, @KEY.UP + if e.shiftKey + # do nothing + return true + + @prevHint() + + e.stopPropagation() + return false + + when @KEY.RIGHT, @KEY.DOWN + if e.shiftKey + # do nothing + return true + + @nextHint() + + e.stopPropagation() + return false + + # allow the event to propagate + return true + + handleClickOnDocument: (e) -> + @hideHint() + + # allow the event to propagate + return true; calculate: -> $.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) -> diff --git a/lms/static/sass/course/layout/_calculator.scss b/lms/static/sass/course/layout/_calculator.scss index 274d8a00c6..b9a5d286ef 100644 --- a/lms/static/sass/course/layout/_calculator.scss +++ b/lms/static/sass/course/layout/_calculator.scss @@ -112,15 +112,20 @@ div.calc-main { right: 0; top: 0; - a { + #calculator_hint { background: url("../images/info-icon.png") center center no-repeat; height: 35px; @include hide-text; width: 35px; - display: block; + display: block; + + &:focus { + outline: 5px auto #5b9dd9; + } } .help { + @include transition(none); background: #fff; border-radius: 3px; box-shadow: 0 0 3px #999; @@ -129,11 +134,12 @@ div.calc-main { position: absolute; right: -40px; bottom: 57px; - @include transition(none); width: 600px; overflow: hidden; pointer-events: none; display: none; + margin: 0; + list-style: none; &.shown { display: block; diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 80b52b9d36..9a6a197de0 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -218,13 +218,14 @@ ${fragment.foot_html()}
- ${_("Hints")} - + +
  • ${_("Operators")}: + - * / ^ and || (${_("parallel resistors function")})

  • +
  • ${_("Functions")}: sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs, fact/factorial

  • +
  • ${_("Constants")}:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    j=sqrt(-1)
    e=${_("Euler's number")}
    pi=${_("ratio of a circle's circumference to it's diameter")}
    k=${_("Boltzmann constant")}
    c=${_("speed of light")}
    T=${_("freezing point of water in degrees Kelvin")}
    q=${_("fundamental charge")}
    +
  • +