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 =
'' +
'' +
- `
${message}
` +
+ '
' +
'
';
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}