|
240 | 240 |
|
241 | 241 | var data = response.data || {}; |
242 | 242 | if (data.code === 'locked_out' && data.remaining > 0) { |
243 | | - startLockoutCountdown(data.remaining); |
| 243 | + var firstTwofaInput = twofaStep |
| 244 | + ? twofaStep.querySelector('input:not([type="hidden"])') |
| 245 | + : null; |
| 246 | + startLockoutCountdown( |
| 247 | + data.remaining, |
| 248 | + twofaErrorBox, |
| 249 | + twofaSubmitBtn, |
| 250 | + firstTwofaInput |
| 251 | + ); |
244 | 252 | } else if (data.delay > 0) { |
245 | 253 | startThrottleCountdown(data.delay, twofaErrorBox, twofaSubmitBtn); |
246 | 254 | } else { |
|
365 | 373 | * Disables the submit button and updates the error message each second |
366 | 374 | * until the lockout expires, then re-enables the form. |
367 | 375 | * |
368 | | - * @param {number} remaining Seconds until lockout expires. |
| 376 | + * @param {number} remaining Seconds until lockout expires. |
| 377 | + * @param {Element} box Optional error box element. |
| 378 | + * @param {Element} btn Optional submit button element. |
| 379 | + * @param {Element} focusInput Optional input to focus when lockout expires. |
369 | 380 | */ |
370 | | - function startLockoutCountdown(remaining) { |
| 381 | + function startLockoutCountdown(remaining, box, btn, focusInput) { |
| 382 | + var targetBox = box || errorBox; |
| 383 | + var targetBtn = btn || submitBtn; |
| 384 | + var targetInput = focusInput || passwordInput; |
| 385 | + |
371 | 386 | if (lockoutInterval) { |
372 | 387 | clearInterval(lockoutInterval); |
373 | 388 | } |
374 | 389 |
|
375 | | - submitBtn.disabled = true; |
| 390 | + if (targetBtn) { |
| 391 | + targetBtn.disabled = true; |
| 392 | + } |
376 | 393 |
|
377 | 394 | // Suppress per-second screen reader announcements during countdown. |
378 | 395 | // The error box has role="alert" which would announce every update. |
379 | 396 | // Instead, we set aria-live="off" and announce only at milestones. |
380 | | - if (errorBox) { |
381 | | - errorBox.setAttribute('aria-live', 'off'); |
| 397 | + if (targetBox) { |
| 398 | + targetBox.setAttribute('aria-live', 'off'); |
382 | 399 | } |
383 | 400 |
|
384 | 401 | function tick() { |
385 | 402 | if (remaining <= 0) { |
386 | 403 | clearInterval(lockoutInterval); |
387 | 404 | lockoutInterval = null; |
388 | | - submitBtn.disabled = false; |
| 405 | + if (targetBtn) { |
| 406 | + targetBtn.disabled = false; |
| 407 | + } |
389 | 408 | // Restore live region before hiding so SR announces clearance. |
390 | | - if (errorBox) { |
391 | | - errorBox.removeAttribute('aria-live'); |
| 409 | + if (targetBox) { |
| 410 | + targetBox.removeAttribute('aria-live'); |
392 | 411 | } |
393 | | - hideError(errorBox); |
| 412 | + hideError(targetBox); |
394 | 413 | announce(strings.lockoutExpired || 'Lockout expired. You may try again.'); |
395 | | - passwordInput.focus(); |
| 414 | + if (targetInput) { |
| 415 | + targetInput.focus(); |
| 416 | + } |
396 | 417 | return; |
397 | 418 | } |
398 | 419 |
|
|
401 | 422 | var timeStr = m + ':' + (s < 10 ? '0' : '') + s; |
402 | 423 |
|
403 | 424 | // Update visual text every second. |
404 | | - var p = errorBox ? errorBox.querySelector('p') : null; |
405 | | - if (errorBox && !errorBox.hidden) { |
| 425 | + var p = targetBox ? targetBox.querySelector('p') : null; |
| 426 | + if (targetBox && !targetBox.hidden) { |
406 | 427 | // Direct DOM update to avoid triggering live region. |
407 | 428 | if (p) { |
408 | 429 | p.textContent = strings.lockoutCountdown.replace('%s', timeStr); |
409 | 430 | } |
410 | 431 | } else { |
411 | | - showError(errorBox, strings.lockoutCountdown.replace('%s', timeStr)); |
| 432 | + showError(targetBox, strings.lockoutCountdown.replace('%s', timeStr)); |
412 | 433 | } |
413 | 434 |
|
414 | 435 | // Announce to screen readers every 30 seconds and at 10 seconds. |
|
0 commit comments