fix(a11y): show the password hint visually but hide it from screen readers

Restores the "e.g. ••••" hint that the previous commit removed, as an
aria-hidden overlay instead of a native placeholder — screen readers
read native placeholders character by character. FormField gets a
decorativePlaceholder prop for the link-share fields, and the link-share
auth field gains the aria-label it never had (its placeholder used to be
its only hint). Re-using the translation key also fixes the dead-key CI
failure.

Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
This commit is contained in:
kolaente
2026-07-19 20:58:13 +02:00
parent 270d37c699
commit cce3dde5ab
5 changed files with 32 additions and 0 deletions
@@ -9,6 +9,10 @@ interface Props {
disabled?: boolean
loading?: boolean
layout?: 'stacked' | 'two-col'
// Shown like a placeholder but hidden from assistive technology —
// screen readers read native placeholders character by character
// for hints like "e.g. ••••••••••••".
decorativePlaceholder?: string
}
const props = withDefaults(defineProps<Props>(), {
@@ -129,6 +133,13 @@ defineExpose({
:aria-describedby="errorId"
@input="handleInput"
>
<span
v-if="decorativePlaceholder && !modelValue"
class="decorative-placeholder"
aria-hidden="true"
>
{{ decorativePlaceholder }}
</span>
</slot>
</div>
<div
@@ -14,6 +14,13 @@
@keyup="() => {validateAfterFirst ? validate() : null}"
@input="handleInput"
>
<span
v-if="password === ''"
class="decorative-placeholder"
aria-hidden="true"
>
{{ $t('user.auth.passwordPlaceholder') }}
</span>
<BaseButton
v-tooltip="passwordFieldType === 'password' ? $t('user.auth.showPassword') : $t('user.auth.hidePassword')"
class="password-field-type-toggle"
@@ -56,6 +56,7 @@
v-model="password"
v-tooltip="$t('project.share.links.passwordExplanation')"
:label="$t('project.share.links.password')"
:decorative-placeholder="$t('user.auth.passwordPlaceholder')"
type="password"
/>
<XButton
+11
View File
@@ -105,3 +105,14 @@ h1, h2, h3 {
height: auto;
}
}
// Visual stand-in for a native placeholder that assistive technology must not
// read (native placeholders are announced character by character).
.decorative-placeholder {
position: absolute;
inset-block-start: 50%;
transform: translateY(-50%);
inset-inline-start: calc(0.75em + 1px);
color: var(--input-placeholder-color);
pointer-events: none;
}
@@ -12,6 +12,8 @@
v-model="password"
v-focus
type="password"
:aria-label="$t('user.auth.password')"
:decorative-placeholder="$t('user.auth.passwordPlaceholder')"
@keyup.enter.prevent="authenticate()"
/>