#168: Formulare — Fehler-Verdrahtung und Namenslücken

Der Field-Baustein verdrahtet Hinweis/Fehler jetzt per aria-describedby
und aria-invalid mit dem Eingabefeld (cloneElement auf das einzelne
Kind; Fragmente bleiben unangetastet) — Screenreader nennen den Fehler
damit auch beim Feld-Fokus. Quota-Typ-Select mit Namen; die leeren
Aktions-/Erledigt-Spaltenköpfe in API-Tokens, Feed-Tokens, Sitzungen
und der Aufgabenübersicht (NodeView UND Server-Renderpfad) tragen
visually-hidden-Beschriftungen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq
This commit is contained in:
Claude Fable 5 2026-07-21 14:26:54 +02:00
parent 4ba7b50336
commit 8719b0ee1e
11 changed files with 40 additions and 10 deletions

View File

@ -171,7 +171,8 @@ export class TasksService {
}
return (
`<table class="dt-task-overview-table"><thead><tr>` +
`<th></th><th>${escapeHtml(t('colTask'))}</th><th>${escapeHtml(t('colMentions'))}</th>` +
`<th><span class="visually-hidden">${escapeHtml(t('colDone'))}</span></th>` +
`<th>${escapeHtml(t('colTask'))}</th><th>${escapeHtml(t('colMentions'))}</th>` +
`<th>${escapeHtml(t('colStart'))}</th><th>${escapeHtml(t('colDue'))}</th>` +
`<th>${escapeHtml(t('colPage'))}</th>` +
`</tr></thead><tbody>${rows.join('')}</tbody></table>`

View File

@ -182,7 +182,9 @@ function TokenList({ tokens }: { tokens: ApiTokenView[] }): React.JSX.Element {
<th>{t('list.lastUsed')}</th>
<th>{t('list.expires')}</th>
<th>{t('list.status')}</th>
<th></th>
<th>
<span className="visually-hidden">{t('common:tableActions')}</span>
</th>
</tr>
</thead>
<tbody>

View File

@ -91,7 +91,9 @@ export function FeedTokensSection(): React.JSX.Element {
<th>{t('fields.name')}</th>
<th>{t('list.created')}</th>
<th>{t('list.lastUsed')}</th>
<th></th>
<th>
<span className="visually-hidden">{t('common:tableActions')}</span>
</th>
</tr>
</thead>
<tbody>

View File

@ -1,3 +1,4 @@
import { cloneElement, isValidElement, useId } from 'react';
import { useTranslation } from 'react-i18next';
import { ApiError } from '../lib/api';
@ -20,13 +21,28 @@ export function Field({
children: React.ReactNode;
}): React.JSX.Element {
const { t } = useTranslation();
const noteId = useId();
// Tie the hint/error text to the control itself (#168, WCAG 3.3.1):
// screen readers then repeat it when the field receives focus. Only a
// single element child can be wired; fragments render unchanged.
const wired =
isValidElement(children) && (error || hint)
? cloneElement(children as React.ReactElement<Record<string, unknown>>, {
'aria-describedby': noteId,
...(error ? { 'aria-invalid': true } : {}),
})
: children;
return (
<label className="field">
<span className="field__label">{label}</span>
{children}
{hint && !error && <span className="field__hint">{hint}</span>}
{wired}
{hint && !error && (
<span className="field__hint" id={noteId}>
{hint}
</span>
)}
{error && (
<span className="field__error" role="alert">
<span className="field__error" role="alert" id={noteId}>
{t(`errors:${error}`, t('errors:bad_request'))}
</span>
)}

View File

@ -86,7 +86,9 @@ function TaskOverviewView({ editor }: NodeViewProps): React.JSX.Element {
<table className="dt-task-overview-table">
<thead>
<tr>
<th></th>
<th>
<span className="visually-hidden">{t('colDone')}</span>
</th>
<th>{t('colTask')}</th>
<th>{t('colMentions')}</th>
<th>{t('colStart')}</th>

View File

@ -63,6 +63,7 @@ export function QuotaManager(): React.JSX.Element {
<select
className="quota-manager__type"
value={type}
aria-label={t('overrides.typeLabel')}
onChange={(e) => setType(e.target.value as QuotaSubject)}
>
<option value="user">{t('overrides.user')}</option>

View File

@ -255,7 +255,9 @@ function SessionsSection(): React.JSX.Element {
<th>{t('settings:sessions.device')}</th>
<th>{t('settings:sessions.created')}</th>
<th>{t('settings:sessions.lastSeen')}</th>
<th></th>
<th>
<span className="visually-hidden">{t('common:tableActions')}</span>
</th>
</tr>
</thead>
<tbody>

View File

@ -25,7 +25,8 @@
"usage": "Nutzung",
"set": "Setzen",
"clear": "Löschen",
"overQuota": "über Kontingent"
"overQuota": "über Kontingent",
"typeLabel": "Geltungsbereich"
},
"keys": {
"editors_per_pond": "Bearbeiter pro Teich",

View File

@ -1,6 +1,7 @@
{
"title": "Aufgabenübersicht",
"editorCard": "Aufgabenübersicht — diese Seite und ihre Unterseiten",
"colDone": "Erledigt",
"colTask": "Aufgabe",
"colMentions": "Wer",
"colStart": "Start",

View File

@ -25,7 +25,8 @@
"usage": "Usage",
"set": "Set",
"clear": "Clear",
"overQuota": "over quota"
"overQuota": "over quota",
"typeLabel": "Applies to"
},
"keys": {
"editors_per_pond": "Editors per pond",

View File

@ -1,6 +1,7 @@
{
"title": "Task overview",
"editorCard": "Task overview — this page and its subpages",
"colDone": "Done",
"colTask": "Task",
"colMentions": "Who",
"colStart": "Start",