in einem dauerhaft (ausser .pz-btn). So schleppt auch der naechste
neu geschriebene Fliesstext-Link den Verstoss nicht wieder ein.
Ausnahmen entfernt: duplicate-id-aria, form-field-multiple-labels,
heading-order und link-in-text-block samt Kommentarbloecken aus .pa11yci.js und
.pa11yci.streng.js. Die color-contrast-Ausnahme auf / bleibt — sie ist keine
Altlast, sondern die dokumentierte Grenze bei Verlaufshintergruenden.
Verifiziert: beide pa11y-Laeufe gruen ohne die Ausnahmen (4/4 URLs, 0 errors),
Label-Zuordnung am gerenderten DOM geprueft (IDs eindeutig, label.control zeigt
je auf das Feld im eigenen Formular, Label-Klick fokussiert das richtige Feld,
Hinweisknoten haengt am ausloesenden Formular).
Co-Authored-By: Claude
Claude-Session: https://claude.ai/code/session_01BervJUSWK8ymMbcuBuu9d9
---
app/.pa11yci.js | 15 +--------
app/.pa11yci.streng.js | 44 +++++--------------------
app/src/app/[tenant]/RegionEinstieg.tsx | 32 ++++++++++++++----
app/src/app/[tenant]/umfragen/page.tsx | 22 ++++++++++---
app/src/app/globals.css | 15 +++++++++
5 files changed, 67 insertions(+), 61 deletions(-)
diff --git a/app/.pa11yci.js b/app/.pa11yci.js
index 9cbf34a..844fe0c 100644
--- a/app/.pa11yci.js
+++ b/app/.pa11yci.js
@@ -109,21 +109,8 @@ module.exports = {
ignore: [],
},
{
- _kommentar_ignore: [
- "ALTLAST, eingefroren am 2026-07-26 — abzubauen, nicht auszuweiten.",
- "Regel: link-in-text-block · Seite: /anliegen · Element:",
- '#main-content > main > p > a → Konto unter',
- "„Meine Anliegen“ . Der Link im Fließtext ist nur farblich (--pz-brand-strong)",
- "vom umgebenden Text unterschieden; 'hover:underline' greift erst beim Hover.",
- "Grund für die Ausnahme: dieser PR friert den Bestand ein und ändert",
- "bewusst KEINE UI (Issue #60). Fix gehört in einen eigenen PR (Unterstreichung",
- "im Ruhezustand oder ausreichender Nicht-Farb-Unterschied); danach diese",
- "Ausnahme in BEIDEN Configs ersatzlos entfernen.",
- "Die Ausnahme gilt regel- UND seitengenau: auf allen anderen URLs blockt",
- "link-in-text-block weiterhin hart.",
- ],
url: url("/anliegen"),
- ignore: ["link-in-text-block"],
+ ignore: [],
},
{
url: url("/anmelden"),
diff --git a/app/.pa11yci.streng.js b/app/.pa11yci.streng.js
index 3a3c3b7..2d26a53 100644
--- a/app/.pa11yci.streng.js
+++ b/app/.pa11yci.streng.js
@@ -32,7 +32,7 @@
* Beide Läufe zusammen decken auf den vier geprüften Seiten alle axe-Regeln des
* Standards WCAG2AA ab, in jeder Impact-Klasse, sowohl `violations` als auch
* `incomplete` — mit der einen dokumentierten Ausnahme `color-contrast`/`/` aus
- * Punkt 3 und den unten eingefrorenen Altlasten. Was das Gate NICHT sieht:
+ * Punkt 3. Sonst gibt es keine Ausnahme mehr. Was das Gate NICHT sieht:
* andere Seiten (siehe .pa11yci.js: nur vier anonyme Sichten), alles was
* Interaktion braucht (Fokus-Reihenfolge, Tastaturbedienung, Live-Regionen nach
* einer Aktion), und alles, was automatisierte Prüfung grundsätzlich nicht
@@ -58,44 +58,16 @@ const AUSNAHMEN = {
* Abbau: nur mit einem einfarbigen Hero-Hintergrund. Kein Ziel für sich —
* echte Kontrast-VERSTÖSSE blockt LAUF 1 auf dieser Seite weiterhin.
*
- * `duplicate-id-aria` — ALTLAST, eingefroren am 2026-07-26.
- * Regel: duplicate-id-aria · Seite: / · Elemente: id="plz" und
- * id="plz-funktion" existieren auf der Startseite je zweimal
- * (Hero-Formular und zweites Formular weiter unten).
- * Grund: 2 belegte Verstöße (Impact critical). Dieser PR baut das GATE und
- * ändert bewusst keine UI-Datei; der Fix ist ein eigener PR.
- * Abbau: IDs eindeutig machen (z. B. Suffix je Formular-Instanz) und diese
- * Ausnahme dann ERSATZLOS entfernen — der Folge-PR nimmt sie mit raus.
- *
- * `form-field-multiple-labels` — ALTLAST, eingefroren am 2026-07-26.
- * Regel: form-field-multiple-labels · Seite: / · Elemente: dieselben zwei
- * PLZ-Felder, die durch die doppelten IDs je zwei einsammeln.
- * Grund: Impact moderate, Folgefehler der doppelten IDs. Gleiche Begründung
- * wie oben: keine UI-Änderung in diesem PR.
- * Abbau: verschwindet mit dem duplicate-id-aria-Fix von selbst; Ausnahme im
- * selben Folge-PR ersatzlos entfernen.
+ * Die vier am 2026-07-26 eingefrorenen Altlasten (`duplicate-id-aria` und
+ * `form-field-multiple-labels` auf `/`, `heading-order` auf `/umfragen`,
+ * `link-in-text-block` auf `/anliegen`) sind behoben und ihre Ausnahmen
+ * ersatzlos entfernt — die Regeln blocken jetzt auf allen Seiten hart.
*/
- "/": ["color-contrast", "duplicate-id-aria", "form-field-multiple-labels"],
+ "/": ["color-contrast"],
- /**
- * `heading-order` — ALTLAST, eingefroren am 2026-07-26.
- * Regel: heading-order · Seite: /umfragen · Element: eine Überschrift
- * springt eine Ebene (h2 → h4) statt lückenlos zu staffeln.
- * Grund: 1 belegter Verstoß (Impact moderate). Erst durch `includeWarnings`
- * in diesem Lauf überhaupt sichtbar geworden. Keine UI-Änderung in
- * diesem PR.
- * Abbau: Überschriftenebene korrigieren, danach diese Ausnahme ersatzlos
- * entfernen.
- */
- "/umfragen": ["heading-order"],
+ "/umfragen": [],
- /**
- * `link-in-text-block` — ALTLAST, eingefroren am 2026-07-26.
- * Identisch zur Ausnahme in .pa11yci.js (dort steht die volle Begründung).
- * Muss in BEIDEN Configs stehen, weil beide Läufe die Seite prüfen, und im
- * selben Folge-PR aus beiden verschwinden.
- */
- "/anliegen": ["link-in-text-block"],
+ "/anliegen": [],
"/anmelden": [],
};
diff --git a/app/src/app/[tenant]/RegionEinstieg.tsx b/app/src/app/[tenant]/RegionEinstieg.tsx
index fbb44e1..ff698e9 100644
--- a/app/src/app/[tenant]/RegionEinstieg.tsx
+++ b/app/src/app/[tenant]/RegionEinstieg.tsx
@@ -8,7 +8,7 @@
* Schwelle, ohne Konto. Die Wahl wird gemerkt; „Region ändern" ist später möglich.
*/
-import { useState, useTransition } from "react";
+import { useId, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import {
regionAusPlz,
@@ -39,6 +39,24 @@ export function RegionEinstieg({
onErfolg?: () => void;
}) {
const router = useRouter();
+ /**
+ * Eindeutige IDs je Instanz (a11y-Gate, Issue #60). Die Komponente steht auf
+ * der Startseite ZWEIMAL im selben Dokument (Hero + CTA-Wiederholung) und
+ * zusätzlich auf /umfragen sowie im Konto. Mit statischen IDs (`plz`,
+ * `plz-funktion`, …) existierte jede ID doppelt: `duplicate-id-aria`
+ * (Impact critical) plus `form-field-multiple-labels` als Folgefehler.
+ * Real spürbar ist das an der Label-Zuordnung — `` zeigt
+ * IMMER auf das erste Feld im Dokument; ein Klick auf das Label des unteren
+ * Formulars springt also nach oben, und Screenreader lesen die Beschreibung
+ * des falschen Feldes vor.
+ * `useId()` statt fester Suffixe, weil ein Suffix nur so lange trägt, wie die
+ * Zahl der Instanzen bekannt ist; useId skaliert auf beliebig viele.
+ */
+ const idBasis = useId();
+ const plzId = `${idBasis}-plz`;
+ const funktionId = `${idBasis}-plz-funktion`;
+ const fehlerId = `${idBasis}-plz-fehler`;
+ const hinweisId = `${idBasis}-plz-hinweis`;
const [plz, setPlz] = useState("");
const [fehler, setFehler] = useState(null);
const [hinweis, setHinweis] = useState(null);
@@ -124,17 +142,17 @@ export function RegionEinstieg({
}
>