From f17d7cb4b2887edfde590644988ceb733e9231ab Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Fri, 22 Sep 2017 16:54:08 +0200 Subject: [PATCH] HFP-1266 Add accessibility labels to controls Improve the overall behavior when using a readspeaker. Still missing localization, dialog support and a proper timer. --- card.js | 65 ++++++++++++++++++++++++++++++++++++++----------- memory-game.css | 8 ++++++ memory-game.js | 62 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 105 insertions(+), 30 deletions(-) diff --git a/card.js b/card.js index 76a1c73..38d1ffe 100644 --- a/card.js +++ b/card.js @@ -21,6 +21,8 @@ var path = H5P.getPath(image.path, id); 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 > image.height) { width = '100%'; @@ -35,10 +37,42 @@ 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. */ self.flip = function () { + if (flippedState) { + $wrapper.blur().focus(); // Announce card label again + return; + } + $card.addClass('h5p-flipped'); self.trigger('flip'); flippedState = true; @@ -48,6 +82,7 @@ * Flip card back. */ self.flipBack = function () { + self.updateLabel(null, null, true); // Reset card label $card.removeClass('h5p-flipped'); flippedState = false; }; @@ -55,7 +90,7 @@ /** * Remove. */ - self.remove = function () { + self.remove = function (announce) { $card.addClass('h5p-matched'); removedState = true; }; @@ -64,6 +99,9 @@ * Reset card to natural state */ self.reset = function () { + self.updateLabel(null, null, true); // Reset card label + flippedState = false; + removedState = false; $card[0].classList.remove('h5p-flipped', 'h5p-matched'); }; @@ -91,11 +129,10 @@ * @param {H5P.jQuery} $container */ self.appendTo = function ($container) { - // TODO: Translate alt attr $wrapper = $('
  • ' + '
    ' + (styles && styles.backImage ? '' : '') + '
    ' + '
    ' + - '' + (alt || 'Memory Image') + '' + + '' + alt + '' + '
    ' + '
  • ') .appendTo($container) @@ -103,10 +140,8 @@ switch (event.which) { case 13: // Enter case 32: // Space - if (!flippedState) { - self.flip(); - event.preventDefault(); - } + self.flip(); + event.preventDefault(); return; case 39: // Right case 40: // Down @@ -122,6 +157,7 @@ return; } }); + $wrapper.attr('aria-label', 'Card ' + ($wrapper.index() + 1) + ': Unturned.'); // TODO l10n $card = $wrapper.children('.h5p-memory-card') .children('.h5p-front') .click(function () { @@ -131,7 +167,7 @@ }; /** - * Re-append to parent container + * Re-append to parent container. */ self.reAppend = function () { var parent = $wrapper[0].parentElement; @@ -139,7 +175,7 @@ }; /** - * + * Make the card accessible when tabbing */ self.makeTabbable = function () { if ($wrapper) { @@ -148,7 +184,7 @@ }; /** - * + * Prevent tabbing to the card */ self.makeUntabbable = function () { if ($wrapper) { @@ -157,7 +193,7 @@ }; /** - * + * Make card tabbable and move focus to it */ self.setFocus = function () { 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 () { - return flippedState; + self.isRemoved = function () { + return removedState; }; }; diff --git a/memory-game.css b/memory-game.css index 199a07b..4d61392 100644 --- a/memory-game.css +++ b/memory-game.css @@ -1,6 +1,14 @@ .h5p-memory-game { 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 { list-style: none !important; padding: 0.25em 0.5em !important; diff --git a/memory-game.js b/memory-game.js index 894f512..a0cb033 100644 --- a/memory-game.js +++ b/memory-game.js @@ -5,6 +5,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { var CARD_STD_SIZE = 116; // PX var STD_FONT_SIZE = 16; // PX var LIST_PADDING = 1; // EMs + var numInstances = 0; /** * Memory Game Constructor @@ -21,11 +22,12 @@ H5P.MemoryGame = (function (EventDispatcher, $) { // Initialize event inheritance 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 flipBacks = []; // Que of cards to be flipped back var numFlipped = 0; var removed = 0; + numInstances++; /** * Check if these two cards belongs together. @@ -49,17 +51,17 @@ H5P.MemoryGame = (function (EventDispatcher, $) { return; } - // Remove them from the game. - card.remove(); - mate.remove(); - // Update counters numFlipped -= 2; removed += 2; 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) { // Pause timer and show desciption. timer.pause(); @@ -70,6 +72,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { popup.show(desc, imgs, cardStyles ? cardStyles.back : undefined, function () { if (isFinished) { // Game done + card.makeUntabbable(); finished(); } else { @@ -80,6 +83,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { } else if (isFinished) { // Game done + card.makeUntabbable(); finished(); } }; @@ -90,7 +94,8 @@ H5P.MemoryGame = (function (EventDispatcher, $) { */ var finished = function () { timer.stop(); - $feedback.addClass('h5p-show'); + $feedback.addClass('h5p-show'); // Announce + $bottom.focus(); // Create and trigger xAPI event 'completed' var completedEvent = self.createXAPIEventTemplate('completed'); @@ -113,7 +118,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { }); retryButton.classList.add('h5p-memory-transin'); setTimeout(function () { - // Remove class on nextTick to get transition effect + // Remove class on nextTick to get transition effectupd retryButton.classList.remove('h5p-memory-transin'); }, 0); @@ -132,9 +137,6 @@ H5P.MemoryGame = (function (EventDispatcher, $) { // Reset cards removed = 0; - for (var i = 0; i < cards.length; i++) { - cards[i].reset(); - } // Remove feedback $feedback[0].classList.remove('h5p-show'); @@ -151,11 +153,15 @@ H5P.MemoryGame = (function (EventDispatcher, $) { for (var i = 0; i < cards.length; i++) { cards[i].reAppend(); } + for (var j = 0; j < cards.length; j++) { + cards[j].reset(); + } // Scale new layout $wrapper.children('ul').children('.h5p-row-break').removeClass('h5p-row-break'); maxWidth = -1; self.trigger('resize'); + cards[0].setFocus(); }, 600); }; @@ -197,6 +203,11 @@ H5P.MemoryGame = (function (EventDispatcher, $) { // Keep track of the number of flipped cards 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) { var matie = flipped; // Reset the flipped card. @@ -239,7 +250,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { return; // No more cards } } - while (nextCard.isFlipped()); + while (nextCard.isRemoved()); card.makeUntabbable(); nextCard.setFocus(); @@ -342,24 +353,43 @@ H5P.MemoryGame = (function (EventDispatcher, $) { } // Add cards to list - var $list = $('