Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions games/static/css/flashcards.css
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,22 @@
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid var(--primary-500, #00262B);
}

.gamesxblock-flashcards .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.flashcard-block:focus-visible {
outline: 2px solid var(--primary-500, #00262B);
outline-offset: 2px;
border-radius: 8px;
}
22 changes: 22 additions & 0 deletions games/static/css/matching.css
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,25 @@
opacity: 1;
visibility: visible;
}

.gamesxblock-matching .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

.matching-box:focus-visible {
outline: 2px solid var(--primary-500, #00262B);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(0, 38, 43, 0.2);
}

.matching-end-screen-content:focus {
outline: none;
}
9 changes: 5 additions & 4 deletions games/static/html/flashcards.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<script type="application/json" id="{{data_element_id}}">{{encoded_mapping|safe}}</script>
<script type="text/template" id="flashcards_decoder_script">{{obf_decoder|safe}}</script>

<div class="gamesxblock gamesxblock-flashcards">
<div class="gamesxblock gamesxblock-flashcards" role="region" aria-label="{% trans 'Flashcard game' %}">
<div class="sr-only" aria-live="assertive" aria-atomic="true" role="alert" id="flashcard-sr-announcements"></div>
<!-- Header -->
<div class="flashcards-header">
<h1 class="flashcards-title">{{title}}</h1>
Expand All @@ -18,15 +19,15 @@ <h1 class="flashcards-start-title">{% trans "Click each card to reveal the defin

<!-- Main Card Area -->
<div class="flashcards-card-wrapper">
<div class="flashcard-block" id="flashcard-card" draggable="false" tabindex="0" role="button" aria-pressed="false">
<div class="flashcard-block" id="flashcard-card" draggable="false" tabindex="0" aria-roledescription="flashcard" aria-label="">
<div class="flashcard-inner">
<div class="flashcard-face flashcard-front">
<div class="flashcard-face flashcard-front" aria-hidden="false">
<div class="flashcard-front-content">
<img id="flashcard-term-image" class="image" alt="Flashcard term" />
<div class="flashcard-text" id="flashcard-term">&nbsp;</div>
</div>
</div>
<div class="flashcard-face flashcard-back">
<div class="flashcard-face flashcard-back" aria-hidden="true">
<div class="flashcard-back-content">
<img id="flashcard-definition-image" class="image" alt="Flashcard definition" />
<div class="flashcard-text" id="flashcard-definition">&nbsp;</div>
Expand Down
15 changes: 9 additions & 6 deletions games/static/html/matching.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
<script type="text/template" id="obf_decoder_script">{{obf_decoder|safe}}</script>

<div class='gamesxblock gamesxblock-matching' data-timed="{{ has_timer|yesno:'true,false' }}">
<div class="sr-only" aria-live="assertive" aria-atomic="true" role="alert" id="matching-sr-announcements"></div>
<div class="matching-header">
<h1 class="matching-title">{{title}}</h1>
</div>

<div class="matching-start-screen">
<h1 class="matching-start-title">{% trans "Match each term with the correct definition" %}</h1>
<button class="matching-start-button" {% if list_length <= 0 %}disabled{% endif %}>{% trans "Start" %}</button>
<div class="matching-loading-spinner">
<div class="matching-loading-spinner" aria-hidden="true">
<div class="spinner"></div>
</div>
</div>
Expand All @@ -23,7 +24,8 @@ <h1 class="matching-start-title">{% trans "Match each term with the correct defi
{% if all_pages %}
{% for item in all_pages.0.left_items %}
<div class="matching-box-wrapper">
<div class="matching-box" data-index="matching-key-{{item.index}}" title="{{item.text}}">
<div class="matching-box" data-index="matching-key-{{item.index}}" data-item-type="term"
title="{{item.text}}" role="button" tabindex="0">
<span class="matching-box-text">{{item.text}}</span>
</div>
</div>
Expand All @@ -36,7 +38,8 @@ <h1 class="matching-start-title">{% trans "Match each term with the correct defi
{% if all_pages %}
{% for item in all_pages.0.right_items %}
<div class="matching-box-wrapper">
<div class="matching-box" data-index="matching-key-{{item.index}}" title="{{item.text}}">
<div class="matching-box" data-index="matching-key-{{item.index}}" data-item-type="definition"
title="{{item.text}}" role="button" tabindex="0">
<span class="matching-box-text">{{item.text}}</span>
</div>
</div>
Expand All @@ -48,7 +51,7 @@ <h1 class="matching-start-title">{% trans "Match each term with the correct defi
<div class="matching-footer">
<div class="matching-progress-wrapper">
{% if total_pages > 1 %}
<svg class="matching-progress-circle" xmlns="http://www.w3.org/2000/svg" width="55" height="55" viewBox="0 0 55 55" fill="none">
<svg class="matching-progress-circle" xmlns="http://www.w3.org/2000/svg" width="55" height="55" viewBox="0 0 55 55" fill="none" aria-hidden="true">
<circle class="matching-progress-bg" cx="27.0213" cy="27.0213" r="25.0213" stroke-width="4" />
<circle class="matching-progress-bar" cx="27.0213" cy="27.0213" r="25.0213" stroke-width="4" />
</svg>
Expand All @@ -75,7 +78,7 @@ <h1 class="matching-start-title">{% trans "Match each term with the correct defi

<div class="matching-end-screen">
<div class="confetti-container"></div>
<div class="matching-end-screen-content">
<div class="matching-end-screen-content" tabindex="-1" role="region" aria-label="{% trans 'Game results' %}">
<h1 class="matching-end-title">{% trans "Congratulations!" %}</h1>
<div class="matching-new-best">
<h1 class="matching-new-end-title" id="matching-current-result">0:00</h1>
Expand Down Expand Up @@ -106,4 +109,4 @@ <h1 class="matching-new-end-title" id="matching-current-result">0:00</h1>
<button class="matching-end-button">{% trans "Play again" %}</button>
</div>
</div>
</div>
</div>
53 changes: 50 additions & 3 deletions games/static/js/src/flashcards.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ function GamesXBlockFlashcardsInit(runtime, element, cards) {
var $prevBtn = $element.find('#flashcard-prev');
var $nextBtn = $element.find('#flashcard-next');
var $inner = $element.find('.flashcard-inner');
var $liveRegion = $element.find('#flashcard-sr-announcements');

function announce(message) {
var el = $liveRegion[0];
el.textContent = '';
// Force reflow so the screen reader registers the cleared state
void el.offsetHeight;
el.textContent = message;
}

function getCardAriaLabel() {
var card = cards[currentIndex];
var isFlipped = $card.hasClass(flipClassName);
if (isFlipped) {
return 'Flashcard ' + (currentIndex + 1) + ' of ' + totalCards + '. Definition: ' + (card.definition || '') + '. Press Enter or Space to flip.';
}
return 'Flashcard ' + (currentIndex + 1) + ' of ' + totalCards + '. Term: ' + (card.term || '') + '. Press Enter or Space to flip.';
}

// Render current card
function renderCard() {
Expand Down Expand Up @@ -67,15 +85,34 @@ function GamesXBlockFlashcardsInit(runtime, element, cards) {
void $inner[0].offsetHeight;
$inner.removeClass('no-transition');
}
$element.find('.flashcard-front').attr('aria-hidden', 'false');
$element.find('.flashcard-back').attr('aria-hidden', 'true');
$card.attr('aria-label', getCardAriaLabel());
announce('Card ' + (currentIndex + 1) + ' of ' + totalCards + '. Term: ' + (card.term || ''));
}

// Flip card
function flipCard() {
if ($card.hasClass(flipClassName)) {
var isFlipped = $card.hasClass(flipClassName);
if (isFlipped) {
$card.removeClass(flipClassName);
$element.find('.flashcard-front').attr('aria-hidden', 'false');
$element.find('.flashcard-back').attr('aria-hidden', 'true');
} else {
$card.addClass(flipClassName);
$element.find('.flashcard-front').attr('aria-hidden', 'true');
$element.find('.flashcard-back').attr('aria-hidden', 'false');
}
var card = cards[currentIndex];
if ($card.hasClass(flipClassName)) {
announce('Definition: ' + (card.definition || ''));
} else {
announce('Term: ' + (card.term || ''));
}
// Update aria-label after announce so it doesn't compete
setTimeout(function() {
$card.attr('aria-label', getCardAriaLabel());
}, 1000);
}

// Navigation
Expand All @@ -99,6 +136,7 @@ function GamesXBlockFlashcardsInit(runtime, element, cards) {
$cardWrapper.addClass('active');
$footer.addClass('active');
renderCard();
$card.focus();
}

// Event handlers
Expand Down Expand Up @@ -138,12 +176,21 @@ function GamesXBlockFlashcardsInit(runtime, element, cards) {
break;
case ' ':
case 'Enter':
e.preventDefault();
flipCard();
// Only flip when the card itself is focused, not buttons
if ($(e.target).is($card) || $card.find(e.target).length) {
e.preventDefault();
flipCard();
}
break;
}
});

// Focus the start button on load and announce the game
setTimeout(function() {
$startButton.focus();
announce('Flashcard game. Press Start to begin.');
}, 300);

// Cleanup on unload
$(element).on('remove', function() {
$(element).off('keydown.flashcards');
Expand Down
Loading
Loading