From d590cdd71493755c40b1d9658475a54a58c69eed Mon Sep 17 00:00:00 2001 From: Ed1s0nZ Date: Thu, 8 Oct 2026 11:52:44 +0800 Subject: [PATCH] fix: place accessible password visibility toggle inside login input --- docs/feature/login-password-eye.md | 4 ++ web/static/css/style.css | 57 ++++++++++++++++++++---- web/static/js/auth.js | 4 +- web/static/js/password-feedback.test.cjs | 8 ++++ web/templates/index.html | 14 ++++-- 5 files changed, 75 insertions(+), 12 deletions(-) diff --git a/docs/feature/login-password-eye.md b/docs/feature/login-password-eye.md index a9504988..98b2a001 100644 --- a/docs/feature/login-password-eye.md +++ b/docs/feature/login-password-eye.md @@ -22,3 +22,7 @@ REQ-003 / AC-003: 图标随状态切换,显示密码/隐藏密码标签和悬 修改 index.html 的按钮内部结构,style.css 的局部布局和交互样式,auth.js 的标签同步,扩充已有 password-feedback.test.cjs 的状态/标签断言。 验证:已有密码反馈测试、JS syntax、diff check,实际浏览器桌面/窄屏与语言、键盘切换检查。回退可 revert 本功能代码提交。发布记录在 CHANGELOG.md Unreleased;不合并或发布。 + +## F4 Implementation + +已替换独立文字按钮为输入框内 20px SVG 眼睛,44px 原生按钮点击区域,登录两输入框最小高度 48px;支持主题色及 focus-visible。JS 不再替换按钮 textContent,使用 aria-label/title 同步现有翻译键;模板标记支持语言重绘。更新 CSS/auth.js 缓存版本。已有密码反馈测试 4/4、JS syntax 和 diff check 通过。 diff --git a/web/static/css/style.css b/web/static/css/style.css index 1479f2da..3e9904f7 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -6551,19 +6551,59 @@ html[data-theme="dark"] .chat-input-container .send-btn:disabled::before { } .login-password-field { - display: flex; - align-items: center; - gap: 8px; + position: relative; } -.login-password-field input { +.login-form .login-password-field input[type="password"], +.login-form .login-password-field input[type="text"] { min-width: 0; - flex: 1; + width: 100%; + padding-inline-end: 52px; } -.login-password-field button { - flex: 0 0 auto; - white-space: nowrap; +.login-password-toggle { + position: absolute; + inset-inline-end: 2px; + top: 50%; + transform: translateY(-50%); + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + padding: 0; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + touch-action: manipulation; +} + +.login-password-toggle:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.login-password-toggle:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: -2px; +} + +.login-password-icon { + width: 20px; + height: 20px; + flex: none; + pointer-events: none; +} + +.login-password-icon--hide, +.login-password-toggle[aria-pressed="true"] .login-password-icon--show { + display: none; +} + +.login-password-toggle[aria-pressed="true"] .login-password-icon--hide { + display: block; } .password-field-error { @@ -6587,6 +6627,7 @@ html[data-theme="dark"] .chat-input-container .send-btn:disabled::before { .login-form input[type="text"], .login-form input[type="password"] { + min-height: 48px; width: 100%; padding: 12px 14px; font-size: 0.9375rem; diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 5298232c..437e0c33 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -100,7 +100,9 @@ function setLoginPasswordVisible(visible) { button.setAttribute('aria-pressed', String(visible)); const key = visible ? 'login.hidePassword' : 'login.showPassword'; button.setAttribute('data-i18n', key); - button.textContent = typeof window.t === 'function' ? window.t(key) : (visible ? '隐藏密码' : '显示密码'); + const label = typeof window.t === 'function' ? window.t(key) : (visible ? '隐藏密码' : '显示密码'); + button.setAttribute('aria-label', label); + button.setAttribute('title', label); } } diff --git a/web/static/js/password-feedback.test.cjs b/web/static/js/password-feedback.test.cjs index a3a59fac..ea227bda 100644 --- a/web/static/js/password-feedback.test.cjs +++ b/web/static/js/password-feedback.test.cjs @@ -42,10 +42,18 @@ test('password preview toggles accessibly and can be reset without changing the h.ctx.toggleLoginPasswordVisibility(); assert.equal(h.fields['login-password'].type,'text'); assert.equal(h.fields['login-password-toggle'].getAttribute('aria-pressed'),'true'); + assert.equal(h.fields['login-password-toggle'].getAttribute('aria-label'),'login.hidePassword'); + assert.equal(h.fields['login-password-toggle'].getAttribute('title'),'login.hidePassword'); h.ctx.setLoginPasswordVisible(false); assert.equal(h.fields['login-password'].type,'password'); assert.equal(h.fields['login-password'].value,'test-only-password'); assert.equal(h.fields['login-password-toggle'].getAttribute('data-i18n'),'login.showPassword'); + assert.equal(h.fields['login-password-toggle'].getAttribute('aria-label'),'login.showPassword'); + assert.equal(h.fields['login-password-toggle'].getAttribute('title'),'login.showPassword'); + assert.equal(h.fields['login-password-toggle'].textContent,undefined); + h.ctx.window.t=undefined; + h.ctx.toggleLoginPasswordVisibility(); + assert.equal(h.fields['login-password-toggle'].getAttribute('aria-label'),'隐藏密码'); }); test('password form has field-specific errors without requesting a save',async () => { const h=harness({'auth-new-password':'short','auth-confirm-password':'different'}); diff --git a/web/templates/index.html b/web/templates/index.html index a7bd1c61..206a2add 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -39,7 +39,7 @@ } })(); - + @@ -69,7 +69,15 @@
- +
@@ -6946,7 +6954,7 @@ - +