frontend: Barrierefreiheit — aria-Labels, Fokusreihenfolge, Live-Region
TASKS Phase 2. Jeder Diagramm-Knoten bekommt in render.js (nodeAria) einen sprechenden, lokalisierten aria-label: Label + Status + Aufwand (inkl. „(angenommen)" beim impliziten M) + Zuständige + Link. Die rein visuellen Badges (Größe, Tags, ↗) sind aria-hidden, damit der Screenreader nicht „M", „anna", „↗" kryptisch doppelt vorliest. Neue a11y*-i18n-Keys in allen 9 Sprachen. Alle Knoten sind fokussierbar (tabindex=0 bzw. der Link) in Dokument-/Lese- reihenfolge; sichtbarer :focus-visible-Rahmen (Petrol). #warn ist eine Live-Region (role=status, aria-live=polite) — neue Warnungen werden angesagt. Verifiziert: aria-label folgt dem Sprachwechsel (DE/EN), Badges aria-hidden, keine visuelle Regression, keine Konsolenfehler. Renderer-Snapshots aktualisiert (29 Tests grün). SPEC §9 (Barrierefreiheit) + CLAUDE ergänzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
32a02021ed
commit
15699c321c
+28
-10
@@ -16,29 +16,47 @@ import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
||||
export function esc(s){
|
||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
/* Escaping für Attributwerte (zusätzlich " -> "). */
|
||||
function attr(s){ return esc(String(s)).replace(/"/g,'"'); }
|
||||
|
||||
/* Barrierefreier Name eines Knotens: Label + Status + Aufwand + Zuständige +
|
||||
Link. Die visuellen Badges (Größe, Tags, ↗) sind aria-hidden — ihre
|
||||
Information steckt hier, sonst würde der Screenreader Kryptisches („M",
|
||||
„anna", „↗") vorlesen. */
|
||||
function nodeAria(n, opts){
|
||||
const { t, cheapPath } = opts;
|
||||
const parts = [n.label];
|
||||
if(n.status) parts.push(t('a11yStatus', {status: t('st_' + n.status.key)}));
|
||||
if(n.size) parts.push(t('a11ySize', {size: n.size}));
|
||||
else if(cheapPath) parts.push(t('a11ySizeImplicit'));
|
||||
if(n.tags && n.tags.length) parts.push(t('a11yTags', {names: n.tags.join(', ')}));
|
||||
if(n.url) parts.push(t('a11yLink'));
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
function nodeHtml(n, extra, opts){
|
||||
const { t, cheapPath } = opts;
|
||||
const need = needsBreakdown(n);
|
||||
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
||||
.filter(Boolean).join(' ');
|
||||
const title = n.status ? ` title="${esc(t('st_' + n.status.key)).replace(/"/g,'"')}"` : '';
|
||||
const title = n.status ? ` title="${attr(t('st_' + n.status.key))}"` : '';
|
||||
const tagsHtml = n.tags && n.tags.length
|
||||
? `<span class="tags">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
||||
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
||||
: '';
|
||||
const implicitTip = esc(t('implicitSizeTooltip')).replace(/"/g,'"');
|
||||
const implicitTip = attr(t('implicitSizeTooltip'));
|
||||
const sizeBadge = n.size
|
||||
? `<span class="size">${n.size}</span>`
|
||||
: (cheapPath ? `<span class="size implicit" title="${implicitTip}">M</span>` : '');
|
||||
? `<span class="size" aria-hidden="true">${n.size}</span>`
|
||||
: (cheapPath ? `<span class="size implicit" aria-hidden="true" title="${implicitTip}">M</span>` : '');
|
||||
const inner = esc(n.label) +
|
||||
(n.url ? '<span class="ext">↗</span>' : '') +
|
||||
(n.url ? '<span class="ext" aria-hidden="true">↗</span>' : '') +
|
||||
sizeBadge +
|
||||
tagsHtml;
|
||||
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
||||
const html = n.url
|
||||
? `<a class="${cls}" href="${esc(n.url).replace(/"/g,'"')}" target="_blank" rel="noopener"${title}>${inner}</a>`
|
||||
: `<div class="${cls}"${title}>${inner}</div>`;
|
||||
const ghostTip = esc(t('ghostTooltip')).replace(/"/g,'"');
|
||||
const ghost = `<div class="ghost-node" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${aria}${title}>${inner}</a>`
|
||||
: `<div class="${cls}" tabindex="0"${aria}${title}>${inner}</div>`;
|
||||
const ghostTip = attr(t('ghostTooltip'));
|
||||
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||
return html + (need ? ghost : '');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user