#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 (
|
return (
|
||||||
`<table class="dt-task-overview-table"><thead><tr>` +
|
`<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('colStart'))}</th><th>${escapeHtml(t('colDue'))}</th>` +
|
||||||
`<th>${escapeHtml(t('colPage'))}</th>` +
|
`<th>${escapeHtml(t('colPage'))}</th>` +
|
||||||
`</tr></thead><tbody>${rows.join('')}</tbody></table>`
|
`</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.lastUsed')}</th>
|
||||||
<th>{t('list.expires')}</th>
|
<th>{t('list.expires')}</th>
|
||||||
<th>{t('list.status')}</th>
|
<th>{t('list.status')}</th>
|
||||||
<th></th>
|
<th>
|
||||||
|
<span className="visually-hidden">{t('common:tableActions')}</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -91,7 +91,9 @@ export function FeedTokensSection(): React.JSX.Element {
|
|||||||
<th>{t('fields.name')}</th>
|
<th>{t('fields.name')}</th>
|
||||||
<th>{t('list.created')}</th>
|
<th>{t('list.created')}</th>
|
||||||
<th>{t('list.lastUsed')}</th>
|
<th>{t('list.lastUsed')}</th>
|
||||||
<th></th>
|
<th>
|
||||||
|
<span className="visually-hidden">{t('common:tableActions')}</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { cloneElement, isValidElement, useId } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { ApiError } from '../lib/api';
|
import { ApiError } from '../lib/api';
|
||||||
@ -20,13 +21,28 @@ export function Field({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<label className="field">
|
<label className="field">
|
||||||
<span className="field__label">{label}</span>
|
<span className="field__label">{label}</span>
|
||||||
{children}
|
{wired}
|
||||||
{hint && !error && <span className="field__hint">{hint}</span>}
|
{hint && !error && (
|
||||||
|
<span className="field__hint" id={noteId}>
|
||||||
|
{hint}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<span className="field__error" role="alert">
|
<span className="field__error" role="alert" id={noteId}>
|
||||||
{t(`errors:${error}`, t('errors:bad_request'))}
|
{t(`errors:${error}`, t('errors:bad_request'))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -86,7 +86,9 @@ function TaskOverviewView({ editor }: NodeViewProps): React.JSX.Element {
|
|||||||
<table className="dt-task-overview-table">
|
<table className="dt-task-overview-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th>
|
||||||
|
<span className="visually-hidden">{t('colDone')}</span>
|
||||||
|
</th>
|
||||||
<th>{t('colTask')}</th>
|
<th>{t('colTask')}</th>
|
||||||
<th>{t('colMentions')}</th>
|
<th>{t('colMentions')}</th>
|
||||||
<th>{t('colStart')}</th>
|
<th>{t('colStart')}</th>
|
||||||
|
|||||||
@ -63,6 +63,7 @@ export function QuotaManager(): React.JSX.Element {
|
|||||||
<select
|
<select
|
||||||
className="quota-manager__type"
|
className="quota-manager__type"
|
||||||
value={type}
|
value={type}
|
||||||
|
aria-label={t('overrides.typeLabel')}
|
||||||
onChange={(e) => setType(e.target.value as QuotaSubject)}
|
onChange={(e) => setType(e.target.value as QuotaSubject)}
|
||||||
>
|
>
|
||||||
<option value="user">{t('overrides.user')}</option>
|
<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.device')}</th>
|
||||||
<th>{t('settings:sessions.created')}</th>
|
<th>{t('settings:sessions.created')}</th>
|
||||||
<th>{t('settings:sessions.lastSeen')}</th>
|
<th>{t('settings:sessions.lastSeen')}</th>
|
||||||
<th></th>
|
<th>
|
||||||
|
<span className="visually-hidden">{t('common:tableActions')}</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
"usage": "Nutzung",
|
"usage": "Nutzung",
|
||||||
"set": "Setzen",
|
"set": "Setzen",
|
||||||
"clear": "Löschen",
|
"clear": "Löschen",
|
||||||
"overQuota": "über Kontingent"
|
"overQuota": "über Kontingent",
|
||||||
|
"typeLabel": "Geltungsbereich"
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"editors_per_pond": "Bearbeiter pro Teich",
|
"editors_per_pond": "Bearbeiter pro Teich",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Aufgabenübersicht",
|
"title": "Aufgabenübersicht",
|
||||||
"editorCard": "Aufgabenübersicht — diese Seite und ihre Unterseiten",
|
"editorCard": "Aufgabenübersicht — diese Seite und ihre Unterseiten",
|
||||||
|
"colDone": "Erledigt",
|
||||||
"colTask": "Aufgabe",
|
"colTask": "Aufgabe",
|
||||||
"colMentions": "Wer",
|
"colMentions": "Wer",
|
||||||
"colStart": "Start",
|
"colStart": "Start",
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
"set": "Set",
|
"set": "Set",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"overQuota": "over quota"
|
"overQuota": "over quota",
|
||||||
|
"typeLabel": "Applies to"
|
||||||
},
|
},
|
||||||
"keys": {
|
"keys": {
|
||||||
"editors_per_pond": "Editors per pond",
|
"editors_per_pond": "Editors per pond",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Task overview",
|
"title": "Task overview",
|
||||||
"editorCard": "Task overview — this page and its subpages",
|
"editorCard": "Task overview — this page and its subpages",
|
||||||
|
"colDone": "Done",
|
||||||
"colTask": "Task",
|
"colTask": "Task",
|
||||||
"colMentions": "Who",
|
"colMentions": "Who",
|
||||||
"colStart": "Start",
|
"colStart": "Start",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user