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,
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
......
......@@ -6,8 +6,11 @@
<div class="input-wrapper">
<input type="text" id="calculator_input" tabindex="-1" />
<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>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true"></div>
<a id="calculator_hint" href="#" role="button" aria-haspopup="true" tabindex="-1">Hints</a>
<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>
<input id="calculator_button" type="submit" title="Calculate" arial-label="Calculate" value="=" tabindex="-1" />
......
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 ->
......
# 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()
@hintPopup
.keydown($.proxy(@handleKeyDownOnHint, @))
$(document).keydown $.proxy(@handleKeyDown, @)
@handleClickOnDocument = $.proxy(@handleClickOnDocument, @)
$('div.help-wrapper')
.focusin($.proxy @helpOnFocus, @)
.focusout($.proxy @helpOnBlur, @)
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) ->
......
......@@ -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;
......
......@@ -218,13 +218,14 @@ ${fragment.foot_html()}
<input type="text" id="calculator_input" title="${_('Calculator Input Field')}" tabindex="-1" />
<div class="help-wrapper">
<a id="calculator_hint" href="#" role="button" aria-describedby="calculator_input_help" tabindex="-1">${_("Hints")}</a>
<div id="calculator_input_help" class="help" role="tooltip" aria-hidden="true">
<p><span class="bold">${_("Integers")}:</span> 2520</p>
<p><span class="bold">${_("Decimals")}:</span> 3.14 or .98</p>
<p><span class="bold">${_("Scientific notation")}:</span> 1.2e-2 (=0.012), -4.4e+5 = -4.4e5 (=-440,000)</p>
<p><span class="bold">${_("Appending SI postfixes")}:</span> 2.25k (=2,250)</p>
<p><span class="bold">${_("Supported SI postfixes")}:</span></p>
<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>
<a id="calculator_hint" href="#" role="button" aria-haspopup="true" tabindex="-1" aria-describedby="hint-instructions">${_("Hints")}</a>
<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>
<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>
<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">
<tbody>
<tr>
......@@ -279,49 +280,51 @@ ${fragment.foot_html()}
</tr>
</tbody>
</table>
<p><span class="bold">${_("Operators")}:</span> + - * / ^ and || (${_("parallel resistors function")})</p>
<p><span class="bold">${_("Functions")}:</span> sin, cos, tan, sqrt, log10, log2, ln, arccos, arcsin, arctan, abs, fact/factorial</p>
<p><span class="bold">${_("Constants")}:</span></p>
<table>
<tbody>
<tr>
<td>j</td>
<td>=</td>
<td>sqrt(-1)</td>
</tr>
<tr>
<td>e</td>
<td>=</td>
<td>${_("Euler's number")}</td>
</tr>
<tr>
<td>pi</td>
<td>=</td>
<td>${_("ratio of a circle's circumference to it's diameter")}</td>
</tr>
<tr>
<td>k</td>
<td>=</td>
<td>${_("Boltzmann constant")}</td>
</tr>
<tr>
<td>c</td>
<td>=</td>
<td>${_("speed of light")}</td>
</tr>
<tr>
<td>T</td>
<td>=</td>
<td>${_("freezing point of water in degrees Kelvin")}</td>
</tr>
<tr>
<td>q</td>
<td>=</td>
<td>${_("fundamental charge")}</td>
</tr>
</tbody>
</table>
</div>
</li>
<li class="hint-item" id="hint-operators" tabindex="-1"><p><span class="bold">${_("Operators")}:</span> + - * / ^ and || (${_("parallel resistors function")})</p></li>
<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>
<li class="hint-item" id="hint-constants" tabindex="-1"><p><span class="bold">${_("Constants")}:</span></p>
<table>
<tbody>
<tr>
<td>j</td>
<td>=</td>
<td>sqrt(-1)</td>
</tr>
<tr>
<td>e</td>
<td>=</td>
<td>${_("Euler's number")}</td>
</tr>
<tr>
<td>pi</td>
<td>=</td>
<td>${_("ratio of a circle's circumference to it's diameter")}</td>
</tr>
<tr>
<td>k</td>
<td>=</td>
<td>${_("Boltzmann constant")}</td>
</tr>
<tr>
<td>c</td>
<td>=</td>
<td>${_("speed of light")}</td>
</tr>
<tr>
<td>T</td>
<td>=</td>
<td>${_("freezing point of water in degrees Kelvin")}</td>
</tr>
<tr>
<td>q</td>
<td>=</td>
<td>${_("fundamental charge")}</td>
</tr>
</tbody>
</table>
</li>
</ul>
</div>
</div>
<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