From 6b425aa583ef2d45527cd87a12d74bc9dec03968 Mon Sep 17 00:00:00 2001 From: Alex Jordan Date: Mon, 27 Jul 2026 15:23:03 -0700 Subject: [PATCH] button to announce remaining time on test Co-Authored-By: Claude Sonnet 5 --- htdocs/js/GatewayQuiz/gateway.js | 54 +++++++++++++++- htdocs/js/GatewayQuiz/gateway.scss | 12 +++- .../ContentGenerator/GatewayQuiz.html.ep | 63 +++++++++++-------- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/htdocs/js/GatewayQuiz/gateway.js b/htdocs/js/GatewayQuiz/gateway.js index ee4024bd7f..4f62b2cfe9 100644 --- a/htdocs/js/GatewayQuiz/gateway.js +++ b/htdocs/js/GatewayQuiz/gateway.js @@ -30,6 +30,49 @@ return date.toISOString().substring(11, 19); }; + // Raise a toast to report the remaining time. + const showTimeToast = (text) => { + const toastContainer = document.createElement('div'); + toastContainer.classList.add( + 'gwAlert', + 'toast-container', + 'position-fixed', + 'top-0', + 'start-50', + 'translate-middle-x', + 'p-3' + ); + toastContainer.innerHTML = + '
' + + '
' + + '' + + '' + + '
' + + '
'; + document.body.prepend(toastContainer); + const bsToast = new bootstrap.Toast(toastContainer.firstElementChild, { delay: 4000 }); + toastContainer.addEventListener('hidden.bs.toast', () => { + bsToast.dispose(); + toastContainer.remove(); + }); + + requestAnimationFrame(() => { + toastContainer.querySelector('.toast-body span').textContent = text; + bsToast.show(); + }); + }; + + // Show the current amount of time remaining in a toast, in response to a click on the announce button. + const announceTimeRemaining = () => { + const remainingTime = serverDueTime - Math.round(new Date().getTime() / 1000) + timeDelta; + const text = + remainingTime >= 0 + ? `${remainingTimeString}${formatTime(remainingTime)}` + : `${remainingTimeString}00:00:00`; + + showTimeToast(text); + }; + const alertToast = (message, delay = 5000) => { const toastContainer = document.createElement('div'); toastContainer.classList.add( @@ -41,13 +84,14 @@ 'translate-middle-x', 'p-3' ); + // Start the body empty and populate its content later, to have reliable capture of the update by screen readers. toastContainer.innerHTML = ''; document.body.prepend(toastContainer); const bsToast = new bootstrap.Toast(toastContainer.firstElementChild, { delay }); @@ -55,7 +99,11 @@ bsToast.dispose(); toastContainer.remove(); }); - bsToast.show(); + + requestAnimationFrame(() => { + toastContainer.querySelector('.toast-body').innerHTML = message; + bsToast.show(); + }); }; // Update the timer @@ -156,6 +204,8 @@ updateTimeDelta(); setInterval(updateTimeDelta, Math.min((parseInt(timerDiv.dataset.sessionTimeout) - 60) * 1000, 2147483646)); + document.getElementById('gwAnnounceTimeBtn')?.addEventListener('click', announceTimeRemaining); + const remainingTime = serverDueTime - browserTime + timeDelta; if (!timerDiv.dataset.acting && remainingTime <= 10 - gracePeriod) { diff --git a/htdocs/js/GatewayQuiz/gateway.scss b/htdocs/js/GatewayQuiz/gateway.scss index bb294426eb..bb0e3fc40f 100644 --- a/htdocs/js/GatewayQuiz/gateway.scss +++ b/htdocs/js/GatewayQuiz/gateway.scss @@ -1,17 +1,25 @@ /* gateway styles */ -#gwTimer { +#gwTimerContainer { position: sticky; width: 15em; top: 2.75rem; left: calc(100% - 15em - 0.75rem); right: 0.75rem; - text-align: center; z-index: 31; } +#gwTimer { + text-align: center; +} + .gwAlert { z-index: 32; + + .toast-header + .toast-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + } } #gwScoreSummary { diff --git a/templates/ContentGenerator/GatewayQuiz.html.ep b/templates/ContentGenerator/GatewayQuiz.html.ep index 7e2cd53ac5..ffa02ca75c 100644 --- a/templates/ContentGenerator/GatewayQuiz.html.ep +++ b/templates/ContentGenerator/GatewayQuiz.html.ep @@ -224,33 +224,42 @@ % % # Print the timer if there is less than 24 hours left. % if ($timeLeft < 86400) { - <%= tag('div', - id => 'gwTimer', - class => 'alert alert-warning p-1', - data => { - server_time => int($submitTime), - server_due_time => $c->{set}->due_date, - session_timeout => $ce->{sessionTimeout}, - course_id => $ce->{courseName}, - grace_period => $ce->{gatewayGracePeriod}, - alert_title => maketext('Test Time Notification'), - alert_three => maketext( - 'You have less than 90 seconds left to complete this assignment. You should finish it soon!'), - alert_two => ('
' . maketext('You have less than 45 seconds left!') . '
') - . ( - ($c->{set}->attempts_per_version > 1 && $c->{attemptNumber} > 0) ? '' - : '
' . maketext('Press "Grade Test" soon!') . '
' - ), - alert_one => ('
' . maketext('You are out of time!') . '
') - . ( - ($c->{set}->attempts_per_version > 1 && $c->{attemptNumber} > 0) ? '' - : '
' . maketext('Press "Grade Test" now!') . '
' - ), - $userID ne $effectiveUserID ? (acting => 1) : () - }, - # '00:00:00' is a placeholder that is replaced by the actual time remaining via javascript. - maketext('Remaining time: [_1]', '00:00:00') - ) =%> +
+ <%= tag('div', + id => 'gwTimer', + class => 'alert alert-warning p-1 mb-0', + 'aria-hidden' => 'true', + data => { + server_time => int($submitTime), + server_due_time => $c->{set}->due_date, + session_timeout => $ce->{sessionTimeout}, + course_id => $ce->{courseName}, + grace_period => $ce->{gatewayGracePeriod}, + alert_title => maketext('Test Time Notification'), + alert_three => maketext( + 'You have less than 90 seconds left to complete this assignment. You should finish it soon!'), + alert_two => ('
' . maketext('You have less than 45 seconds left!') . '
') + . ( + ($c->{set}->attempts_per_version > 1 && $c->{attemptNumber} > 0) ? '' + : '
' . maketext('Press "Grade Test" soon!') . '
' + ), + alert_one => ('
' . maketext('You are out of time!') . '
') + . ( + ($c->{set}->attempts_per_version > 1 && $c->{attemptNumber} > 0) ? '' + : '
' . maketext('Press "Grade Test" now!') . '
' + ), + $userID ne $effectiveUserID ? (acting => 1) : () + }, + # '00:00:00' is a placeholder that is replaced by the actual time remaining via javascript. + maketext('Remaining time: [_1]', '00:00:00') + ) =%> + <%= tag('button', + id => 'gwAnnounceTimeBtn', + type => 'button', + class => 'btn btn-sm btn-secondary', + maketext('Announce time remaining') + ) =%> +
% } % % if ($timeLeft < 60 && $timeLeft > 0 && !$c->{can}{gradeUnsubmittedTest}