#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:
parent
4ba7b50336
commit
8719b0ee1e
@ -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>`
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "Aufgabenübersicht",
|
||||
"editorCard": "Aufgabenübersicht — diese Seite und ihre Unterseiten",
|
||||
"colDone": "Erledigt",
|
||||
"colTask": "Aufgabe",
|
||||
"colMentions": "Wer",
|
||||
"colStart": "Start",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"title": "Task overview",
|
||||
"editorCard": "Task overview — this page and its subpages",
|
||||
"colDone": "Done",
|
||||
"colTask": "Task",
|
||||
"colMentions": "Who",
|
||||
"colStart": "Start",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user