Two issues after the autofill fix: - The first box carried a permanent accent border, so once the user moved focus to another box it looked like two boxes were focused. Remove the :first-child highlight; only the actually-focused box is highlighted now. - Typing in the last box did not advance focus, so out-of-order entry could strand the user there with earlier boxes still empty. Add advanceFocus(), which moves to the next empty box forward and then wraps to the start, so after the sixth box focus jumps to the first still-empty box. Used by both single-digit input and the paste/autofill fill path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@@ -1309,7 +1309,6 @@ sub app_html {
|
||
| 1309 | 1309 |
background: #f8fafc; caret-color: transparent; outline: none; |
| 1310 | 1310 |
transition: border-color .15s, background .15s; |
| 1311 | 1311 |
} |
| 1312 |
- .otp-row input:first-child { border-color: var(--accent); }
|
|
| 1313 | 1312 |
.otp-row input:focus { border-color: var(--accent); background: #fff; }
|
| 1314 | 1313 |
.otp-row input.filled { border-color: #b3c6f0; background: #fff; }
|
| 1315 | 1314 |
#login-error {
|
@@ -2024,6 +2023,18 @@ sub app_html {
|
||
| 2024 | 2023 |
otpDigits[idx].classList.toggle('filled', !!digit);
|
| 2025 | 2024 |
} |
| 2026 | 2025 |
|
| 2026 |
+ // Move focus to the next empty box: forward from idx, then wrapping to the |
|
| 2027 |
+ // start. This lets out-of-order entry continue (e.g. after the last box, |
|
| 2028 |
+ // jump back to the first still-empty box). Stays put when all boxes are full. |
|
| 2029 |
+ function advanceFocus(idx) {
|
|
| 2030 |
+ for (let i = idx + 1; i < otpDigits.length; i++) {
|
|
| 2031 |
+ if (!otpDigits[i].value) { otpDigits[i].focus(); return; }
|
|
| 2032 |
+ } |
|
| 2033 |
+ for (let i = 0; i <= idx; i++) {
|
|
| 2034 |
+ if (!otpDigits[i].value) { otpDigits[i].focus(); return; }
|
|
| 2035 |
+ } |
|
| 2036 |
+ } |
|
| 2037 |
+ |
|
| 2027 | 2038 |
// Spread multiple digits across boxes starting at startIdx. Used for paste |
| 2028 | 2039 |
// and for Safari OTP autofill, which drops the whole code into the first box. |
| 2029 | 2040 |
function fillOtp(text, startIdx = 0) {
|
@@ -2035,8 +2046,7 @@ sub app_html {
|
||
| 2035 | 2046 |
setOtpDigit(last, digits[i]); |
| 2036 | 2047 |
} |
| 2037 | 2048 |
syncOtpFields(); |
| 2038 |
- if (last < otpDigits.length - 1) otpDigits[last + 1].focus(); |
|
| 2039 |
- else otpDigits[last].focus(); |
|
| 2049 |
+ advanceFocus(last); |
|
| 2040 | 2050 |
maybeSubmitOtp(); |
| 2041 | 2051 |
} |
| 2042 | 2052 |
|
@@ -2064,7 +2074,7 @@ sub app_html {
|
||
| 2064 | 2074 |
} |
| 2065 | 2075 |
setOtpDigit(idx, input.value); |
| 2066 | 2076 |
syncOtpFields(); |
| 2067 |
- if (input.value && idx < otpDigits.length - 1) otpDigits[idx + 1].focus(); |
|
| 2077 |
+ if (input.value) advanceFocus(idx); |
|
| 2068 | 2078 |
maybeSubmitOtp(); |
| 2069 | 2079 |
}); |
| 2070 | 2080 |
|