diff --git a/.Jules/palette.md b/.Jules/palette.md new file mode 100644 index 0000000000..d23258ade1 --- /dev/null +++ b/.Jules/palette.md @@ -0,0 +1,3 @@ +## 2025-05-14 - Password Visibility Toggle Accessibility +**Learning:** Icon-only buttons for toggling password visibility are often missing `aria-label` and `aria-pressed` states, making them unusable for screen reader users who need to verify their input. +**Action:** Always include dynamic `aria-label` (Switching between "Show password" and "Hide password") and `aria-pressed` state for password toggles. diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 115c1def0c..208e397454 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -334,6 +334,12 @@ export function LoginPage() { type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors" + aria-label={ + showPassword + ? t('hidePassword', language) + : t('showPassword', language) + } + aria-pressed={showPassword} > {showPassword ? : } diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index cf92819c0f..88dd785391 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -248,6 +248,12 @@ export function RegisterPage() { type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors" + aria-label={ + showPassword + ? t('hidePassword', language) + : t('showPassword', language) + } + aria-pressed={showPassword} > {showPassword ? : } @@ -269,6 +275,12 @@ export function RegisterPage() { type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-600 hover:text-zinc-400 transition-colors" + aria-label={ + showConfirmPassword + ? t('hidePassword', language) + : t('showPassword', language) + } + aria-pressed={showConfirmPassword} > {showConfirmPassword ? : } diff --git a/web/src/components/ResetPasswordPage.tsx b/web/src/components/ResetPasswordPage.tsx index 2504c9c8a3..7d9b74ba5c 100644 --- a/web/src/components/ResetPasswordPage.tsx +++ b/web/src/components/ResetPasswordPage.tsx @@ -150,6 +150,12 @@ export function ResetPasswordPage() { onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon" style={{ color: 'var(--text-secondary)' }} + aria-label={ + showPassword + ? t('hidePassword', language) + : t('showPassword', language) + } + aria-pressed={showPassword} > {showPassword ? ( @@ -184,6 +190,12 @@ export function ResetPasswordPage() { } className="absolute inset-y-0 right-2 w-8 h-10 flex items-center justify-center btn-icon" style={{ color: 'var(--text-secondary)' }} + aria-label={ + showConfirmPassword + ? t('hidePassword', language) + : t('showPassword', language) + } + aria-pressed={showConfirmPassword} > {showConfirmPassword ? ( diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 7d44356e76..eb96949cf9 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -667,6 +667,8 @@ export const translations = { passwordRequired: 'Password is required', invalidEmail: 'Invalid email format', passwordTooShort: 'Password must be at least 6 characters', + showPassword: 'Show password', + hidePassword: 'Hide password', // Landing Page features: 'Features', @@ -1834,6 +1836,8 @@ export const translations = { passwordRequired: '请输入密码', invalidEmail: '邮箱格式不正确', passwordTooShort: '密码至少需要6个字符', + showPassword: '显示密码', + hidePassword: '隐藏密码', // Landing Page features: '功能',