HFP-1266 Add accessibility labels to controls

Improve the overall behavior when using a readspeaker.
Still missing localization, dialog support and a proper timer.
pull/30/head^2
Frode Petterson 2017-09-22 16:54:08 +02:00
parent 196c112243
commit f17d7cb4b2
3 changed files with 105 additions and 30 deletions

65
card.js
View File

@ -21,6 +21,8 @@
var path = H5P.getPath(image.path, id); var path = H5P.getPath(image.path, id);
var width, height, margin, $card, $wrapper, removedState, flippedState; var width, height, margin, $card, $wrapper, removedState, flippedState;
alt = alt || 'Missing description'; // Default for old games
if (image.width !== undefined && image.height !== undefined) { if (image.width !== undefined && image.height !== undefined) {
if (image.width > image.height) { if (image.width > image.height) {
width = '100%'; width = '100%';
@ -35,10 +37,42 @@
width = height = '100%'; width = height = '100%';
} }
/**
* Update the cards label to make it accessible to users with a readspeaker
*
* @param {boolean} isMatched The card has been matched
* @param {boolean} announce Announce the current state of the card
* @param {boolean} reset Go back to the default label
*/
self.updateLabel = function (isMatched, announce, reset) {
// Determine new label from input params
var label = (reset ? 'Unturned' : alt);
if (isMatched) {
label = 'Match found. ' + label; // TODO l10n
}
// Update the card's label
$wrapper.attr('aria-label', 'Card ' + ($wrapper.index() + 1) + ': ' + label); // TODO l10n
// Update disabled property
$wrapper.attr('aria-disabled', reset ? null : 'true');
// Announce the label change
if (announce) {
$wrapper.blur().focus(); // Announce card label
}
};
/** /**
* Flip card. * Flip card.
*/ */
self.flip = function () { self.flip = function () {
if (flippedState) {
$wrapper.blur().focus(); // Announce card label again
return;
}
$card.addClass('h5p-flipped'); $card.addClass('h5p-flipped');
self.trigger('flip'); self.trigger('flip');
flippedState = true; flippedState = true;
@ -48,6 +82,7 @@
* Flip card back. * Flip card back.
*/ */
self.flipBack = function () { self.flipBack = function () {
self.updateLabel(null, null, true); // Reset card label
$card.removeClass('h5p-flipped'); $card.removeClass('h5p-flipped');
flippedState = false; flippedState = false;
}; };
@ -55,7 +90,7 @@
/** /**
* Remove. * Remove.
*/ */
self.remove = function () { self.remove = function (announce) {
$card.addClass('h5p-matched'); $card.addClass('h5p-matched');
removedState = true; removedState = true;
}; };
@ -64,6 +99,9 @@
* Reset card to natural state * Reset card to natural state
*/ */
self.reset = function () { self.reset = function () {
self.updateLabel(null, null, true); // Reset card label
flippedState = false;
removedState = false;
$card[0].classList.remove('h5p-flipped', 'h5p-matched'); $card[0].classList.remove('h5p-flipped', 'h5p-matched');
}; };
@ -91,11 +129,10 @@
* @param {H5P.jQuery} $container * @param {H5P.jQuery} $container
*/ */
self.appendTo = function ($container) { self.appendTo = function ($container) {
// TODO: Translate alt attr
$wrapper = $('<li class="h5p-memory-wrap" tabindex="-1" role="button"><div class="h5p-memory-card">' + $wrapper = $('<li class="h5p-memory-wrap" tabindex="-1" role="button"><div class="h5p-memory-card">' +
'<div class="h5p-front"' + (styles && styles.front ? styles.front : '') + '>' + (styles && styles.backImage ? '' : '<span></span>') + '</div>' + '<div class="h5p-front"' + (styles && styles.front ? styles.front : '') + '>' + (styles && styles.backImage ? '' : '<span></span>') + '</div>' +
'<div class="h5p-back"' + (styles && styles.back ? styles.back : '') + '>' + '<div class="h5p-back"' + (styles && styles.back ? styles.back : '') + '>' +
'<img src="' + path + '" alt="' + (alt || 'Memory Image') + '" style="width:' + width + ';height:' + height + '"/>' + '<img src="' + path + '" alt="' + alt + '" style="width:' + width + ';height:' + height + '"/>' +
'</div>' + '</div>' +
'</div></li>') '</div></li>')
.appendTo($container) .appendTo($container)
@ -103,10 +140,8 @@
switch (event.which) { switch (event.which) {
case 13: // Enter case 13: // Enter
case 32: // Space case 32: // Space
if (!flippedState) { self.flip();
self.flip(); event.preventDefault();
event.preventDefault();
}
return; return;
case 39: // Right case 39: // Right
case 40: // Down case 40: // Down
@ -122,6 +157,7 @@
return; return;
} }
}); });
$wrapper.attr('aria-label', 'Card ' + ($wrapper.index() + 1) + ': Unturned.'); // TODO l10n
$card = $wrapper.children('.h5p-memory-card') $card = $wrapper.children('.h5p-memory-card')
.children('.h5p-front') .children('.h5p-front')
.click(function () { .click(function () {
@ -131,7 +167,7 @@
}; };
/** /**
* Re-append to parent container * Re-append to parent container.
*/ */
self.reAppend = function () { self.reAppend = function () {
var parent = $wrapper[0].parentElement; var parent = $wrapper[0].parentElement;
@ -139,7 +175,7 @@
}; };
/** /**
* * Make the card accessible when tabbing
*/ */
self.makeTabbable = function () { self.makeTabbable = function () {
if ($wrapper) { if ($wrapper) {
@ -148,7 +184,7 @@
}; };
/** /**
* * Prevent tabbing to the card
*/ */
self.makeUntabbable = function () { self.makeUntabbable = function () {
if ($wrapper) { if ($wrapper) {
@ -157,7 +193,7 @@
}; };
/** /**
* * Make card tabbable and move focus to it
*/ */
self.setFocus = function () { self.setFocus = function () {
self.makeTabbable(); self.makeTabbable();
@ -167,10 +203,11 @@
}; };
/** /**
* * Check if the card has been removed from the game, i.e. if has
* been matched.
*/ */
self.isFlipped = function () { self.isRemoved = function () {
return flippedState; return removedState;
}; };
}; };

View File

@ -1,6 +1,14 @@
.h5p-memory-game { .h5p-memory-game {
overflow: hidden; overflow: hidden;
} }
.h5p-memory-game .h5p-memory-hidden-read {
position: absolute;
top: -1px;
left: -1px;
width: 1px;
height: 1px;
color: transparent;
}
.h5p-memory-game > ul { .h5p-memory-game > ul {
list-style: none !important; list-style: none !important;
padding: 0.25em 0.5em !important; padding: 0.25em 0.5em !important;

View File

@ -5,6 +5,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
var CARD_STD_SIZE = 116; // PX var CARD_STD_SIZE = 116; // PX
var STD_FONT_SIZE = 16; // PX var STD_FONT_SIZE = 16; // PX
var LIST_PADDING = 1; // EMs var LIST_PADDING = 1; // EMs
var numInstances = 0;
/** /**
* Memory Game Constructor * Memory Game Constructor
@ -21,11 +22,12 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
// Initialize event inheritance // Initialize event inheritance
EventDispatcher.call(self); EventDispatcher.call(self);
var flipped, timer, counter, popup, $feedback, $wrapper, maxWidth, numCols; var flipped, timer, counter, popup, $bottom, $feedback, $wrapper, maxWidth, numCols;
var cards = []; var cards = [];
var flipBacks = []; // Que of cards to be flipped back var flipBacks = []; // Que of cards to be flipped back
var numFlipped = 0; var numFlipped = 0;
var removed = 0; var removed = 0;
numInstances++;
/** /**
* Check if these two cards belongs together. * Check if these two cards belongs together.
@ -49,17 +51,17 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
return; return;
} }
// Remove them from the game.
card.remove();
mate.remove();
// Update counters // Update counters
numFlipped -= 2; numFlipped -= 2;
removed += 2; removed += 2;
var isFinished = (removed === cards.length); var isFinished = (removed === cards.length);
var desc = card.getDescription();
// Remove them from the game.
card.remove(!isFinished);
mate.remove();
var desc = card.getDescription();
if (desc !== undefined) { if (desc !== undefined) {
// Pause timer and show desciption. // Pause timer and show desciption.
timer.pause(); timer.pause();
@ -70,6 +72,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
popup.show(desc, imgs, cardStyles ? cardStyles.back : undefined, function () { popup.show(desc, imgs, cardStyles ? cardStyles.back : undefined, function () {
if (isFinished) { if (isFinished) {
// Game done // Game done
card.makeUntabbable();
finished(); finished();
} }
else { else {
@ -80,6 +83,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
} }
else if (isFinished) { else if (isFinished) {
// Game done // Game done
card.makeUntabbable();
finished(); finished();
} }
}; };
@ -90,7 +94,8 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
*/ */
var finished = function () { var finished = function () {
timer.stop(); timer.stop();
$feedback.addClass('h5p-show'); $feedback.addClass('h5p-show'); // Announce
$bottom.focus();
// Create and trigger xAPI event 'completed' // Create and trigger xAPI event 'completed'
var completedEvent = self.createXAPIEventTemplate('completed'); var completedEvent = self.createXAPIEventTemplate('completed');
@ -113,7 +118,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
}); });
retryButton.classList.add('h5p-memory-transin'); retryButton.classList.add('h5p-memory-transin');
setTimeout(function () { setTimeout(function () {
// Remove class on nextTick to get transition effect // Remove class on nextTick to get transition effectupd
retryButton.classList.remove('h5p-memory-transin'); retryButton.classList.remove('h5p-memory-transin');
}, 0); }, 0);
@ -132,9 +137,6 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
// Reset cards // Reset cards
removed = 0; removed = 0;
for (var i = 0; i < cards.length; i++) {
cards[i].reset();
}
// Remove feedback // Remove feedback
$feedback[0].classList.remove('h5p-show'); $feedback[0].classList.remove('h5p-show');
@ -151,11 +153,15 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
for (var i = 0; i < cards.length; i++) { for (var i = 0; i < cards.length; i++) {
cards[i].reAppend(); cards[i].reAppend();
} }
for (var j = 0; j < cards.length; j++) {
cards[j].reset();
}
// Scale new layout // Scale new layout
$wrapper.children('ul').children('.h5p-row-break').removeClass('h5p-row-break'); $wrapper.children('ul').children('.h5p-row-break').removeClass('h5p-row-break');
maxWidth = -1; maxWidth = -1;
self.trigger('resize'); self.trigger('resize');
cards[0].setFocus();
}, 600); }, 600);
}; };
@ -197,6 +203,11 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
// Keep track of the number of flipped cards // Keep track of the number of flipped cards
numFlipped++; numFlipped++;
// Announce the card unless it's the last one and it's correct
var isMatched = (flipped === mate);
var isLast = ((removed + 2) === cards.length);
card.updateLabel(isMatched, !(isMatched && isLast));
if (flipped !== undefined) { if (flipped !== undefined) {
var matie = flipped; var matie = flipped;
// Reset the flipped card. // Reset the flipped card.
@ -239,7 +250,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
return; // No more cards return; // No more cards
} }
} }
while (nextCard.isFlipped()); while (nextCard.isRemoved());
card.makeUntabbable(); card.makeUntabbable();
nextCard.setFocus(); nextCard.setFocus();
@ -342,24 +353,43 @@ H5P.MemoryGame = (function (EventDispatcher, $) {
} }
// Add cards to list // Add cards to list
var $list = $('<ul/>'); var $list = $('<ul/>', {
role: 'application',
'aria-labelledby': 'h5p-intro-' + numInstances
});
for (var i = 0; i < cards.length; i++) { for (var i = 0; i < cards.length; i++) {
cards[i].appendTo($list); cards[i].appendTo($list);
} }
cards[0].makeTabbable(); cards[0].makeTabbable();
if ($list.children().length) { if ($list.children().length) {
$('<div/>', {
id: 'h5p-intro-' + numInstances,
'class': 'h5p-memory-hidden-read',
html: 'Memory Game. Find the matching cards.', // TODO: l10n
appendTo: $container
});
$list.appendTo($container); $list.appendTo($container);
$feedback = $('<div class="h5p-feedback">' + parameters.l10n.feedback + '</div>').appendTo($container); $bottom = $('<div/>', {
tabindex: '-1',
appendTo: $container
});
$('<div/>', {
'class': 'h5p-memory-hidden-read',
html: 'All of the cards have been found.', // TODO: l10n
appendTo: $bottom
});
$feedback = $('<div class="h5p-feedback">' + parameters.l10n.feedback + '</div>').appendTo($bottom);
// Add status bar // Add status bar
var $status = $('<dl class="h5p-status">' + var $status = $('<dl class="h5p-status">' +
'<dt>' + parameters.l10n.timeSpent + '</dt>' + '<dt>' + parameters.l10n.timeSpent + '</dt>' +
'<dd class="h5p-time-spent">0:00</dd>' + '<dd class="h5p-time-spent">0:00</dd>' + // TODO: Add hidden dot ?
'<dt>' + parameters.l10n.cardTurns + '</dt>' + '<dt>' + parameters.l10n.cardTurns + '</dt>' +
'<dd class="h5p-card-turns">0</dd>' + '<dd class="h5p-card-turns">0</dd>' +
'</dl>').appendTo($container); '</dl>').appendTo($bottom);
timer = new MemoryGame.Timer($status.find('.h5p-time-spent')[0]); timer = new MemoryGame.Timer($status.find('.h5p-time-spent')[0]);
counter = new MemoryGame.Counter($status.find('.h5p-card-turns')); counter = new MemoryGame.Counter($status.find('.h5p-card-turns'));