Commit 0dbb7603 by polesye

BLD-533: Improve calculator's tooltip accessibility.

parent ef528494
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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 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 task, with reports uploaded to S3. Feature is visible on the beta
instructor dashboard. LMS-58 instructor dashboard. LMS-58
......
...@@ -6,8 +6,11 @@ ...@@ -6,8 +6,11 @@
<div class="input-wrapper"> <div class="input-wrapper">
<input type="text" id="calculator_input" tabindex="-1" /> <input type="text" id="calculator_input" tabindex="-1" />
<div class="help-wrapper"> <div class="help-wrapper">
<a id="calculator_hint" href="#" role="button" aria-haspopup="true" aria-controls="calculator_input_help" aria-expanded="false" tabindex="-1">Hints</a> <a id="calculator_hint" href="#" role="button" aria-haspopup="true" tabindex="-1">Hints</a>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true"></div> <ul id="calculator_input_help" class="help" aria-activedescendant="hint-integers" role="tooltip" aria-hidden="true">
<li class="hint-item" id="hint-integers" tabindex="-1"><p><span class="bold">Integers:</span> 2520</p></li>
<li class="hint-item" id="hint-decimals" tabindex="-1"><p><span class="bold">Decimals:</span> 3.14 or .98</p></li>
</ul>
</div> </div>
</div> </div>
<input id="calculator_button" type="submit" title="Calculate" arial-label="Calculate" value="=" tabindex="-1" /> <input id="calculator_button" type="submit" title="Calculate" arial-label="Calculate" value="=" tabindex="-1" />
......
describe 'Calculator', -> describe 'Calculator', ->
KEY =
TAB : 9
ENTER : 13
ALT : 18
ESC : 27
SPACE : 32
LEFT : 37
UP : 38
RIGHT : 39
DOWN : 40
beforeEach -> beforeEach ->
loadFixtures 'coffee/fixtures/calculator.html' loadFixtures 'coffee/fixtures/calculator.html'
@calculator = new Calculator @calculator = new Calculator
...@@ -9,15 +21,14 @@ describe 'Calculator', -> ...@@ -9,15 +21,14 @@ describe 'Calculator', ->
it 'bind the help button', -> it 'bind the help button', ->
# These events are bind by $.hover() # These events are bind by $.hover()
expect($('div.help-wrapper a')).toHandle 'mouseover' expect($('#calculator_hint')).toHandle 'mouseover'
expect($('div.help-wrapper a')).toHandle 'mouseout' expect($('#calculator_hint')).toHandle 'mouseout'
expect($('div.help-wrapper')).toHandle 'focusin' expect($('#calculator_hint')).toHandle 'keydown'
expect($('div.help-wrapper')).toHandle 'focusout'
it 'prevent default behavior on help button', -> it 'prevent default behavior on help button', ->
$('div.help-wrapper a').click (e) -> $('#calculator_hint').click (e) ->
expect(e.isDefaultPrevented()).toBeTruthy() expect(e.isDefaultPrevented()).toBeTruthy()
$('div.help-wrapper a').click() $('#calculator_hint').click()
it 'bind the calculator submit', -> it 'bind the calculator submit', ->
expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate expect($('form#calculator')).toHandleWith 'submit', @calculator.calculate
...@@ -51,30 +62,261 @@ describe 'Calculator', -> ...@@ -51,30 +62,261 @@ describe 'Calculator', ->
@calculator.toggle(jQuery.Event("click")) @calculator.toggle(jQuery.Event("click"))
expect($('.calc')).not.toHaveClass('closed') expect($('.calc')).not.toHaveClass('closed')
describe 'helpShow', -> describe 'showHint', ->
it 'show the help overlay', -> it 'show the help overlay', ->
@calculator.helpShow() @calculator.showHint()
expect($('.help')).toHaveClass('shown') expect($('.help')).toHaveClass('shown')
expect($('.help')).toHaveAttr('aria-hidden', 'false') expect($('.help')).toHaveAttr('aria-hidden', 'false')
describe 'helpHide', ->
describe 'hideHint', ->
it 'show the help overlay', -> it 'show the help overlay', ->
@calculator.helpHide() @calculator.hideHint()
expect($('.help')).not.toHaveClass('shown') expect($('.help')).not.toHaveClass('shown')
expect($('.help')).toHaveAttr('aria-hidden', 'true') expect($('.help')).toHaveAttr('aria-hidden', 'true')
describe 'handleKeyDown', -> describe 'handleClickOnDocument', ->
it 'on pressing Esc the hint becomes hidden', -> it 'on click out of the hint popup it becomes hidden', ->
@calculator.helpShow() @calculator.showHint()
e = jQuery.Event('keydown', { which: 27 } ); e = jQuery.Event('click');
$(document).trigger(e); $(document).trigger(e);
expect($('.help')).not.toHaveClass 'shown' expect($('.help')).not.toHaveClass 'shown'
it 'On pressing other buttons the hint continue to show', -> describe 'selectHint', ->
@calculator.helpShow() it 'select correct hint item', ->
e = jQuery.Event('keydown', { which: 32 } ); spyOn($.fn, 'focus')
$(document).trigger(e); element = $('.hint-item').eq(1)
expect($('.help')).toHaveClass 'shown' @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', -> describe 'calculate', ->
beforeEach -> beforeEach ->
......
# 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 class @Calculator
constructor: -> constructor: ->
@hintButton = $('#calculator_hint')
@hintPopup = $('.help')
@hintsList = @hintPopup.find('.hint-item')
@selectHint($('#' + @hintPopup.attr('aria-activedescendant')));
$('.calc').click @toggle $('.calc').click @toggle
$('form#calculator').submit(@calculate).submit (e) -> $('form#calculator').submit(@calculate).submit (e) ->
e.preventDefault() e.preventDefault()
$('div.help-wrapper a') @hintButton
.hover( .hover(
$.proxy(@helpShow, @), $.proxy(@showHint, @),
$.proxy(@helpHide, @) $.proxy(@hideHint, @)
) )
.click (e) -> .keydown($.proxy(@handleKeyDown, @))
e.preventDefault() .click (e) -> e.preventDefault()
@hintPopup
.keydown($.proxy(@handleKeyDownOnHint, @))
$(document).keydown $.proxy(@handleKeyDown, @) @handleClickOnDocument = $.proxy(@handleClickOnDocument, @)
$('div.help-wrapper') KEY:
.focusin($.proxy @helpOnFocus, @) TAB : 9
.focusout($.proxy @helpOnBlur, @) ENTER : 13
ESC : 27
SPACE : 32
LEFT : 37
UP : 38
RIGHT : 39
DOWN : 40
toggle: (event) -> toggle: (event) ->
event.preventDefault() event.preventDefault()
...@@ -49,32 +76,110 @@ class @Calculator ...@@ -49,32 +76,110 @@ class @Calculator
$calc.toggleClass 'closed' $calc.toggleClass 'closed'
helpOnFocus: (e) -> showHint: ->
e.preventDefault() @hintPopup
@isFocusedHelp = true
@helpShow()
helpOnBlur: (e) ->
e.preventDefault()
@isFocusedHelp = false
@helpHide()
helpShow: ->
$('.help')
.addClass('shown') .addClass('shown')
.attr('aria-hidden', false) .attr('aria-hidden', false)
helpHide: -> $(document).on('click', @handleClickOnDocument)
if not @isFocusedHelp
$('.help') hideHint: ->
.removeClass('shown') @hintPopup
.attr('aria-hidden', true) .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) -> handleKeyDown: (e) ->
ESC = 27 if e.altKey
if e.which is ESC and $('.help').hasClass 'shown' # do nothing
@isFocusedHelp = false return true
@helpHide()
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: -> calculate: ->
$.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) -> $.getWithPrefix '/calculate', { equation: $('#calculator_input').val() }, (data) ->
......
...@@ -112,15 +112,20 @@ div.calc-main { ...@@ -112,15 +112,20 @@ div.calc-main {
right: 0; right: 0;
top: 0; top: 0;
a { #calculator_hint {
background: url("../images/info-icon.png") center center no-repeat; background: url("../images/info-icon.png") center center no-repeat;
height: 35px; height: 35px;
@include hide-text; @include hide-text;
width: 35px; width: 35px;
display: block; display: block;
&:focus {
outline: 5px auto #5b9dd9;
}
} }
.help { .help {
@include transition(none);
background: #fff; background: #fff;
border-radius: 3px; border-radius: 3px;
box-shadow: 0 0 3px #999; box-shadow: 0 0 3px #999;
...@@ -129,11 +134,12 @@ div.calc-main { ...@@ -129,11 +134,12 @@ div.calc-main {
position: absolute; position: absolute;
right: -40px; right: -40px;
bottom: 57px; bottom: 57px;
@include transition(none);
width: 600px; width: 600px;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
display: none; display: none;
margin: 0;
list-style: none;
&.shown { &.shown {
display: block; display: block;
......
...@@ -218,13 +218,14 @@ ${fragment.foot_html()} ...@@ -218,13 +218,14 @@ ${fragment.foot_html()}
<input type="text" id="calculator_input" title="${_('Calculator Input Field')}" tabindex="-1" /> <input type="text" id="calculator_input" title="${_('Calculator Input Field')}" tabindex="-1" />
<div class="help-wrapper"> <div class="help-wrapper">
<a id="calculator_hint" href="#" role="button" aria-describedby="calculator_input_help" tabindex="-1">${_("Hints")}</a> <p class="sr" id="hint-instructions">${_('Use the arrow keys to navigate the tips or use the tab key to return to the calculator')}</p>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true"> <a id="calculator_hint" href="#" role="button" aria-haspopup="true" tabindex="-1" aria-describedby="hint-instructions">${_("Hints")}</a>
<p><span class="bold">${_("Integers")}:</span> 2520</p> <ul id="calculator_input_help" class="help" aria-activedescendant="hint-integers" role="tooltip" aria-hidden="true">
<p><span class="bold">${_("Decimals")}:</span> 3.14 or .98</p> <li class="hint-item" id="hint-integers" tabindex="-1"><p><span class="bold">${_("Integers")}:</span> 2520</p></li>
<p><span class="bold">${_("Scientific notation")}:</span> 1.2e-2 (=0.012), -4.4e+5 = -4.4e5 (=-440,000)</p> <li class="hint-item" id="hint-decimals" tabindex="-1"><p><span class="bold">${_("Decimals")}:</span> 3.14 or .98</p></li>
<p><span class="bold">${_("Appending SI postfixes")}:</span> 2.25k (=2,250)</p> <li class="hint-item" id="hint-scientific-notation" tabindex="-1"><p><span class="bold">${_("Scientific notation")}:</span> 1.2e-2 (=0.012), -4.4e+5 = -4.4e5 (=-440,000)</p></li>
<p><span class="bold">${_("Supported SI postfixes")}:</span></p> <li class="hint-item" id="hint-appending-postfixes" tabindex="-1"><p><span class="bold">${_("Appending SI postfixes")}:</span> 2.25k (=2,250)</p></li>
<li class="hint-item" id="hint-supported-postfixes" tabindex="-1"><p><span class="bold">${_("Supported SI postfixes")}:</span></p>
<table class="calc-postfixes"> <table class="calc-postfixes">
<tbody> <tbody>
<tr> <tr>
...@@ -279,49 +280,51 @@ ${fragment.foot_html()} ...@@ -279,49 +280,51 @@ ${fragment.foot_html()}
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p><span class="bold">${_("Operators")}:</span> + - * / ^ and || (${_("parallel resistors function")})</p> </li>
<p><span class="bold">${_("Functions")}:</span> sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs, fact/factorial</p> <li class="hint-item" id="hint-operators" tabindex="-1"><p><span class="bold">${_("Operators")}:</span> + - * / ^ and || (${_("parallel resistors function")})</p></li>
<p><span class="bold">${_("Constants")}:</span></p> <li class="hint-item" id="hint-functions" tabindex="-1"><p><span class="bold">${_("Functions")}:</span> sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs, fact/factorial</p></li>
<table> <li class="hint-item" id="hint-constants" tabindex="-1"><p><span class="bold">${_("Constants")}:</span></p>
<tbody> <table>
<tr> <tbody>
<td>j</td> <tr>
<td>=</td> <td>j</td>
<td>sqrt(-1)</td> <td>=</td>
</tr> <td>sqrt(-1)</td>
<tr> </tr>
<td>e</td> <tr>
<td>=</td> <td>e</td>
<td>${_("Euler's number")}</td> <td>=</td>
</tr> <td>${_("Euler's number")}</td>
<tr> </tr>
<td>pi</td> <tr>
<td>=</td> <td>pi</td>
<td>${_("ratio of a circle's circumference to it's diameter")}</td> <td>=</td>
</tr> <td>${_("ratio of a circle's circumference to it's diameter")}</td>
<tr> </tr>
<td>k</td> <tr>
<td>=</td> <td>k</td>
<td>${_("Boltzmann constant")}</td> <td>=</td>
</tr> <td>${_("Boltzmann constant")}</td>
<tr> </tr>
<td>c</td> <tr>
<td>=</td> <td>c</td>
<td>${_("speed of light")}</td> <td>=</td>
</tr> <td>${_("speed of light")}</td>
<tr> </tr>
<td>T</td> <tr>
<td>=</td> <td>T</td>
<td>${_("freezing point of water in degrees Kelvin")}</td> <td>=</td>
</tr> <td>${_("freezing point of water in degrees Kelvin")}</td>
<tr> </tr>
<td>q</td> <tr>
<td>=</td> <td>q</td>
<td>${_("fundamental charge")}</td> <td>=</td>
</tr> <td>${_("fundamental charge")}</td>
</tbody> </tr>
</table> </tbody>
</div> </table>
</li>
</ul>
</div> </div>
</div> </div>
<input id="calculator_button" type="submit" title="${_('Calculate')}" value="=" aria-label="${_('Calculate')}" value="=" tabindex="-1" /> <input id="calculator_button" type="submit" title="${_('Calculate')}" value="=" aria-label="${_('Calculate')}" value="=" tabindex="-1" />
......
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