From 196c112243c81ead6a7c7c7013424d8c774cc8e1 Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Tue, 20 Jun 2017 16:47:07 +0200 Subject: [PATCH 1/5] Start improving accessibility --- card.js | 86 +++++++++++++++++++++++++++++++++++++++++++------- memory-game.js | 39 +++++++++++++++++++++-- semantics.json | 17 +++++++++- 3 files changed, 126 insertions(+), 16 deletions(-) diff --git a/card.js b/card.js index 660a9bf..76a1c73 100644 --- a/card.js +++ b/card.js @@ -7,10 +7,11 @@ * @extends H5P.EventDispatcher * @param {Object} image * @param {number} id + * @param {string} alt * @param {string} [description] * @param {Object} [styles] */ - MemoryGame.Card = function (image, id, description, styles) { + MemoryGame.Card = function (image, id, alt, description, styles) { /** @alias H5P.MemoryGame.Card# */ var self = this; @@ -18,7 +19,7 @@ EventDispatcher.call(self); var path = H5P.getPath(image.path, id); - var width, height, margin, $card; + var width, height, margin, $card, $wrapper, removedState, flippedState; if (image.width !== undefined && image.height !== undefined) { if (image.width > image.height) { @@ -40,6 +41,7 @@ self.flip = function () { $card.addClass('h5p-flipped'); self.trigger('flip'); + flippedState = true; }; /** @@ -47,6 +49,7 @@ */ self.flipBack = function () { $card.removeClass('h5p-flipped'); + flippedState = false; }; /** @@ -54,6 +57,7 @@ */ self.remove = function () { $card.addClass('h5p-matched'); + removedState = true; }; /** @@ -88,27 +92,85 @@ */ self.appendTo = function ($container) { // TODO: Translate alt attr - $card = $('
  • ' + + $wrapper = $('
  • ' + '
    ' + (styles && styles.backImage ? '' : '') + '
    ' + '
    ' + - 'Memory Card' + + '' + (alt || 'Memory Image') + '' + '
    ' + '
  • ') .appendTo($container) - .children('.h5p-memory-card') - .children('.h5p-front') - .click(function () { - self.flip(); - }) - .end(); + .on('keydown', function (event) { + switch (event.which) { + case 13: // Enter + case 32: // Space + if (!flippedState) { + self.flip(); + event.preventDefault(); + } + return; + case 39: // Right + case 40: // Down + // Move focus forward + self.trigger('next'); + event.preventDefault(); + return; + case 37: // Left + case 38: // Up + // Move focus back + self.trigger('prev'); + event.preventDefault(); + return; + } + }); + $card = $wrapper.children('.h5p-memory-card') + .children('.h5p-front') + .click(function () { + self.flip(); + }) + .end(); }; /** * Re-append to parent container */ self.reAppend = function () { - var parent = $card[0].parentElement.parentElement; - parent.appendChild($card[0].parentElement); + var parent = $wrapper[0].parentElement; + parent.appendChild($wrapper[0]); + }; + + /** + * + */ + self.makeTabbable = function () { + if ($wrapper) { + $wrapper.attr('tabindex', '0'); + } + }; + + /** + * + */ + self.makeUntabbable = function () { + if ($wrapper) { + $wrapper.attr('tabindex', '-1'); + } + }; + + /** + * + */ + self.setFocus = function () { + self.makeTabbable(); + if ($wrapper) { + $wrapper.focus(); + } + }; + + /** + * + */ + self.isFlipped = function () { + return flippedState; }; }; diff --git a/memory-game.js b/memory-game.js index a0029f6..894f512 100644 --- a/memory-game.js +++ b/memory-game.js @@ -220,6 +220,38 @@ H5P.MemoryGame = (function (EventDispatcher, $) { counter.increment(); }); + /** + * @private + * @param {number} direction + */ + var createCardChangeFocusHandler = function (direction) { + return function () { + // Locate next card + for (var i = 0; i < cards.length; i++) { + if (cards[i] === card) { + // Found current card + + var nextCard, fails = 0; + do { + fails++; + nextCard = cards[i + (direction * fails)]; + if (!nextCard) { + return; // No more cards + } + } + while (nextCard.isFlipped()); + + card.makeUntabbable(); + nextCard.setFocus(); + + return; + } + } + }; + }; + + card.on('next', createCardChangeFocusHandler(1)); + card.on('prev', createCardChangeFocusHandler(-1)); cards.push(card); }; @@ -277,16 +309,16 @@ H5P.MemoryGame = (function (EventDispatcher, $) { var cardParams = cardsToUse[i]; if (MemoryGame.Card.isValid(cardParams)) { // Create first card - var cardTwo, cardOne = new MemoryGame.Card(cardParams.image, id, cardParams.description, cardStyles); + var cardTwo, cardOne = new MemoryGame.Card(cardParams.image, id, cardParams.imageAlt, cardParams.description, cardStyles); if (MemoryGame.Card.hasTwoImages(cardParams)) { // Use matching image for card two - cardTwo = new MemoryGame.Card(cardParams.match, id, cardParams.description, cardStyles); + cardTwo = new MemoryGame.Card(cardParams.match, id, cardParams.matchAlt, cardParams.description, cardStyles); cardOne.hasTwoImages = cardTwo.hasTwoImages = true; } else { // Add two cards with the same image - cardTwo = new MemoryGame.Card(cardParams.image, id, cardParams.description, cardStyles); + cardTwo = new MemoryGame.Card(cardParams.image, id, cardParams.imageAlt, cardParams.description, cardStyles); } // Add cards to card list for shuffeling @@ -314,6 +346,7 @@ H5P.MemoryGame = (function (EventDispatcher, $) { for (var i = 0; i < cards.length; i++) { cards[i].appendTo($list); } + cards[0].makeTabbable(); if ($list.children().length) { $list.appendTo($container); diff --git a/semantics.json b/semantics.json index fd274e2..52a1f10 100644 --- a/semantics.json +++ b/semantics.json @@ -26,6 +26,13 @@ "importance": "high", "ratio": 1 }, + { + "name": "imageAlt", + "type": "text", + "label": "Alternative text for Image", + "importance": "high", + "description": "Describe what can be seen in the photo. The text is read by text-to-speech tools needed by visually impaired users." + }, { "name": "match", "type": "image", @@ -35,6 +42,14 @@ "description": "An optional image to match against instead of using two cards with the same image.", "ratio": 1 }, + { + "name": "matchAlt", + "type": "text", + "label": "Alternative text for Matching Image", + "importance": "low", + "optional": true, + "description": "Describe what can be seen in the photo. The text is read by text-to-speech tools needed by visually impaired users." + }, { "name": "description", "type": "text", @@ -146,7 +161,7 @@ "importance": "low", "name": "tryAgain", "type": "text", - "default": "Try again?" + "default": "Reset" }, { "label": "Close button label", From f17d7cb4b2887edfde590644988ceb733e9231ab Mon Sep 17 00:00:00 2001 From: Frode Petterson Date: Fri, 22 Sep 2017 16:54:08 +0200 Subject: [PATCH 2/5] 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 = $('