Kundenansicht neu E-Fix M3: Fehlermeldungen der Bestellseiten und Formulare als Live-Region (role="alert")
abo-error, switch-error (und der Fehler der Wechsel-Vorschau), storage-error, tk-error samt Fehlerzeile der Rückfrage-Antwort, der Fehler von "Zahlungsart aktualisieren" sowie login-error und form-error auf Login, Kündigen und Widerrufen waren einfache Absätze: Screenreader lasen sie nicht vor, obwohl der Knopf wieder frei wurde. Jetzt role="alert", gepinnt in test/knEfixCss.test.js. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ddaf2879c4
commit
4c64edc057
9 files changed
+32
-10
No files matched your search
@@ -99,7 +99,7 @@ function bestellung(daten, productKey, { viewAs, kundenart }) {
|
|||||||
], { id: 'abo-einzelheiten' })
|
], { id: 'abo-einzelheiten' })
|
||||||
|
|
||||||
const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null
|
const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null
|
||||||
const fehler = h('p', { class: K.fehler, id: 'abo-error', hidden: true })
|
const fehler = h('p', { class: K.fehler, id: 'abo-error', role: 'alert', hidden: true })
|
||||||
const kaufen = h('button', { type: 'button', class: K.primaer, id: 'abo-buy', disabled: viewAs }, T.knopf || KNOPF)
|
const kaufen = h('button', { type: 'button', class: K.primaer, id: 'abo-buy', disabled: viewAs }, T.knopf || KNOPF)
|
||||||
const zeigeFehler = (text) => { fehler.textContent = text; fehler.hidden = false }
|
const zeigeFehler = (text) => { fehler.textContent = text; fehler.hidden = false }
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function antragsFormular(grenzen, email, T) {
|
|||||||
h('p', { class: 'kn-hinweis' }, 'Tipp: Lade auch die Kündigungsbestätigung deines bisherigen Anbieters hoch – daraus sehen wir, wann der Vertrag dort endet.'),
|
h('p', { class: 'kn-hinweis' }, 'Tipp: Lade auch die Kündigungsbestätigung deines bisherigen Anbieters hoch – daraus sehen wir, wann der Vertrag dort endet.'),
|
||||||
feld('E-Mail-Adresse für unsere Antwort', h('input', { id: 'tk-email', class: K.input, type: 'email', maxlength: 255, autocomplete: 'email', value: email })),
|
feld('E-Mail-Adresse für unsere Antwort', h('input', { id: 'tk-email', class: K.input, type: 'email', maxlength: 255, autocomplete: 'email', value: email })),
|
||||||
baueLinkzeile(T.gutschrift.satz, { id: 'tk-satz', neuerTab: true }),
|
baueLinkzeile(T.gutschrift.satz, { id: 'tk-satz', neuerTab: true }),
|
||||||
h('p', { class: K.fehler, id: 'tk-error', hidden: true }),
|
h('p', { class: K.fehler, id: 'tk-error', role: 'alert', hidden: true }),
|
||||||
h('div', { class: 'kn-aktionen' },
|
h('div', { class: 'kn-aktionen' },
|
||||||
h('button', { type: 'submit', class: K.primaer, id: 'tk-submit' }, T.gutschrift.knopf || 'Antrag absenden'),
|
h('button', { type: 'submit', class: K.primaer, id: 'tk-submit' }, T.gutschrift.knopf || 'Antrag absenden'),
|
||||||
h('button', { type: 'button', class: K.knopf, id: 'tk-cancel' }, 'Abbrechen')))
|
h('button', { type: 'button', class: K.knopf, id: 'tk-cancel' }, 'Abbrechen')))
|
||||||
@@ -102,7 +102,7 @@ function antrag(r, viewAs) {
|
|||||||
h('form', { class: 'kn-tk-antwort', 'data-reply-for': r.id, novalidate: true },
|
h('form', { class: 'kn-tk-antwort', 'data-reply-for': r.id, novalidate: true },
|
||||||
h('textarea', { name: 'message', class: K.textarea, maxlength: 4000, placeholder: 'Deine Antwort…' }),
|
h('textarea', { name: 'message', class: K.textarea, maxlength: 4000, placeholder: 'Deine Antwort…' }),
|
||||||
h('input', { type: 'file', name: 'files', accept: 'application/pdf,image/jpeg,image/png', multiple: true, 'aria-label': 'Weitere Nachweise' }),
|
h('input', { type: 'file', name: 'files', accept: 'application/pdf,image/jpeg,image/png', multiple: true, 'aria-label': 'Weitere Nachweise' }),
|
||||||
h('p', { class: K.fehler, hidden: true }),
|
h('p', { class: K.fehler, role: 'alert', hidden: true }),
|
||||||
h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, disabled: viewAs }, 'Antwort senden')))]
|
h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, disabled: viewAs }, 'Antwort senden')))]
|
||||||
} else {
|
} else {
|
||||||
koerper = [h('p', { class: 'kn-hinweis' }, 'Wir prüfen deine Angaben und antworten dir per E-Mail.')]
|
koerper = [h('p', { class: 'kn-hinweis' }, 'Wir prüfen deine Angaben und antworten dir per E-Mail.')]
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) {
|
|||||||
summary = h('div', { class: 'kn-speicher-zusammenfassung kn-bestellen' },
|
summary = h('div', { class: 'kn-speicher-zusammenfassung kn-bestellen' },
|
||||||
baueBlock(zeilen, { id: 'storage-block' }),
|
baueBlock(zeilen, { id: 'storage-block' }),
|
||||||
plus ? baueLinkzeile(T.speicher.linkzeile, { id: 'storage-linkzeile' }) : baueLinkzeile(T.speicher.warnung, { id: 'storage-warnung', klasse: 'kn-speicher-warnung' }),
|
plus ? baueLinkzeile(T.speicher.linkzeile, { id: 'storage-linkzeile' }) : baueLinkzeile(T.speicher.warnung, { id: 'storage-warnung', klasse: 'kn-speicher-warnung' }),
|
||||||
h('p', { class: 'kn-fehler', id: 'storage-error', hidden: !fehlerText }, fehlerText),
|
h('p', { class: 'kn-fehler', id: 'storage-error', role: 'alert', hidden: !fehlerText }, fehlerText),
|
||||||
h('div', { class: 'kn-aktionen' },
|
h('div', { class: 'kn-aktionen' },
|
||||||
h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? (T.knopf || KNOPF) : 'Speichern')),
|
h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? (T.knopf || KNOPF) : 'Speichern')),
|
||||||
plus ? baueKundenartZeile(kundenart, T.kundenart, { id: 'storage-kundenart' }) : null)
|
plus ? baueKundenartZeile(kundenart, T.kundenart, { id: 'storage-kundenart' }) : null)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
|
|||||||
const zeichne = () => {
|
const zeichne = () => {
|
||||||
const p = lauf.preview
|
const p = lauf.preview
|
||||||
const uebersicht = lauf.error
|
const uebersicht = lauf.error
|
||||||
? h('p', { class: K.fehler }, lauf.error)
|
? h('p', { class: K.fehler, role: 'alert' }, lauf.error)
|
||||||
: lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Deine Übersicht wird berechnet …')
|
: lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Deine Übersicht wird berechnet …')
|
||||||
: baueBlock(zeilenVon(p), { id: 'switch-block' })
|
: baueBlock(zeilenVon(p), { id: 'switch-block' })
|
||||||
const sperre = lauf.busy || viewAs
|
const sperre = lauf.busy || viewAs
|
||||||
@@ -131,7 +131,7 @@ export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
|
|||||||
baueLinkzeile(T.wechsel.linkzeile, { id: 'switch-linkzeile' }),
|
baueLinkzeile(T.wechsel.linkzeile, { id: 'switch-linkzeile' }),
|
||||||
haken ? h('label', { class: K.check }, haken, h('span', { id: 'switch-beginn-text' }, T.wechsel.haken)) : null,
|
haken ? h('label', { class: K.check }, haken, h('span', { id: 'switch-beginn-text' }, T.wechsel.haken)) : null,
|
||||||
gesperrt ? h('p', { class: 'kn-sperrzeile', id: 'switch-sperre' }, T.wechsel.gesperrt) : null,
|
gesperrt ? h('p', { class: 'kn-sperrzeile', id: 'switch-sperre' }, T.wechsel.gesperrt) : null,
|
||||||
h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', hidden: !lauf.fehler }, lauf.fehler),
|
h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', role: 'alert', hidden: !lauf.fehler }, lauf.fehler),
|
||||||
h('div', { class: 'kn-aktionen' },
|
h('div', { class: 'kn-aktionen' },
|
||||||
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
|
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
|
||||||
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<input type="date" id="effective-date" class="hidden">
|
<input type="date" id="effective-date" class="hidden">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<p class="kn-fehlertext hidden" id="form-error"></p>
|
<p class="kn-fehlertext hidden" id="form-error" role="alert"></p>
|
||||||
<button type="submit" class="kn-absenden">Jetzt kündigen</button>
|
<button type="submit" class="kn-absenden">Jetzt kündigen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
enterkeyhint="go"
|
enterkeyhint="go"
|
||||||
>
|
>
|
||||||
<p id="login-error" class="kn-fehlertext hidden"></p>
|
<p id="login-error" class="kn-fehlertext hidden" role="alert"></p>
|
||||||
<!-- Mitarbeiter-Login ist gesperrt: Link ins Ticketsystem (Adresse aus der Antwort, ticketUrl) -->
|
<!-- Mitarbeiter-Login ist gesperrt: Link ins Ticketsystem (Adresse aus der Antwort, ticketUrl) -->
|
||||||
<a id="login-staff-link" class="kn-login-staff hidden" href="https://ticket.webklar.com">Zum Ticketsystem</a>
|
<a id="login-staff-link" class="kn-login-staff hidden" href="https://ticket.webklar.com">Zum Ticketsystem</a>
|
||||||
<button type="submit" id="login-btn" class="kn-login-knopf">
|
<button type="submit" id="login-btn" class="kn-login-knopf">
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<label for="email" data-label-for="email" data-label="E-Mail für die Eingangsbestätigung">E-Mail-Adresse <small>(dorthin schicken wir die Eingangsbestätigung)</small></label>
|
<label for="email" data-label-for="email" data-label="E-Mail für die Eingangsbestätigung">E-Mail-Adresse <small>(dorthin schicken wir die Eingangsbestätigung)</small></label>
|
||||||
<input id="email" name="email" type="email" autocomplete="email" required maxlength="255">
|
<input id="email" name="email" type="email" autocomplete="email" required maxlength="255">
|
||||||
|
|
||||||
<p class="kn-fehlertext hidden" id="form-error"></p>
|
<p class="kn-fehlertext hidden" id="form-error" role="alert"></p>
|
||||||
<button type="submit" class="kn-absenden">Widerruf bestätigen</button>
|
<button type="submit" class="kn-absenden">Widerruf bestätigen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null,
|
|||||||
|
|
||||||
// "Zahlungsart aktualisieren": Stripe-Kundenportal; im Ansichtsmodus gesperrt
|
// "Zahlungsart aktualisieren": Stripe-Kundenportal; im Ansichtsmodus gesperrt
|
||||||
function zahlungsKnopf(viewAs) {
|
function zahlungsKnopf(viewAs) {
|
||||||
const fehler = h('p', { class: K.fehler, hidden: true })
|
const fehler = h('p', { class: K.fehler, role: 'alert', hidden: true })
|
||||||
const knopf = h('button', { type: 'button', class: K.primaer, disabled: viewAs }, 'Zahlungsart aktualisieren')
|
const knopf = h('button', { type: 'button', class: K.primaer, disabled: viewAs }, 'Zahlungsart aktualisieren')
|
||||||
knopf.addEventListener('click', async () => {
|
knopf.addEventListener('click', async () => {
|
||||||
if (viewAs) return
|
if (viewAs) return
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { test } from 'node:test'
|
import { test } from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import { lies, handyTeil } from './support/knHuelle.js'
|
import { lies, handyTeil } from './support/knHuelle.js'
|
||||||
|
|
||||||
// Abschlussprüfung Phase E (Fix-Welle): Pins für die CSS-Befunde der neuen Seite. Sie lesen nur Quelltext, kein Browser.
|
// Abschlussprüfung Phase E (Fix-Welle): Pins für die CSS-Befunde der neuen Seite. Sie lesen nur Quelltext, kein Browser.
|
||||||
@@ -53,3 +54,24 @@ test('F3: Seitenränder der öffentlichen Seiten und des Chats berücksichtigen
|
|||||||
for (const sel of ['.kn-login-kopf', '.kn-login-haupt', '.kn-recht main', '.ab-seite .ab-innen']) assert.ok(hatInsets(oeff, sel), `oeffentlich.css: ${sel}`)
|
for (const sel of ['.kn-login-kopf', '.kn-login-haupt', '.kn-recht main', '.ab-seite .ab-innen']) assert.ok(hatInsets(oeff, sel), `oeffentlich.css: ${sel}`)
|
||||||
assert.ok(hatInsets(lies('chat-composer.css'), '.chat-ov-top'), 'chat-composer.css: .chat-ov-top')
|
assert.ok(hatInsets(lies('chat-composer.css'), '.chat-ov-top'), 'chat-composer.css: .chat-ov-top')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// M3: Fehlermeldungen, die erst nach einer Aktion erscheinen, sind Live-Regionen (role="alert"), damit Screenreader sie vorlesen
|
||||||
|
const quelle = (datei) => readFileSync(new URL(`../public/neu/${datei}`, import.meta.url), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
test('M3: versteckte Fehlerzeilen der Bestellseiten, des Wechsels, des Speichers, der Gutschrift und der Zahlungsart tragen role="alert"', () => {
|
||||||
|
for (const datei of ['ansicht-bestellen.js', 'ansicht-wechsel.js', 'ansicht-speicher.js', 'ansicht-gutschrift.js', 'vertraege-abo.js']) {
|
||||||
|
const q = quelle(datei)
|
||||||
|
// jedes Attributobjekt eines Elements mit der Fehlerklasse, das versteckt starten kann (hidden), hat eine Rolle
|
||||||
|
const objekte = [...q.matchAll(/\{[^{}]*(?:K\.fehler|'kn-fehler'|fehlerKlasse)[^{}]*\}/g)].map((m) => m[0]).filter((o) => /\bhidden\b/.test(o))
|
||||||
|
assert.ok(objekte.length > 0, `${datei}: keine versteckte Fehlerzeile gefunden`)
|
||||||
|
for (const o of objekte) assert.match(o, /role:\s*'alert'/, `${datei}: ${o}`)
|
||||||
|
}
|
||||||
|
// der Fehler der Wechsel-Vorschau steht nicht versteckt, wird aber nach dem Laden eingesetzt: auch er ist eine Live-Region
|
||||||
|
assert.match(quelle('ansicht-wechsel.js'), /h\('p', \{ class: K\.fehler, role: 'alert' \}, lauf\.error\)/)
|
||||||
|
})
|
||||||
|
test('M3: die Fehlertexte der öffentlichen Seiten (Login, Kündigen, Widerrufen) sind Live-Regionen', () => {
|
||||||
|
for (const datei of ['login.html', 'kuendigen.html', 'widerrufen.html']) {
|
||||||
|
const html = readFileSync(new URL(`../public/neu/oeffentlich/${datei}`, import.meta.url), 'utf8')
|
||||||
|
const zeile = html.match(/<p[^>]*id="(?:login|form)-error"[^>]*>/)?.[0] ?? ''
|
||||||
|
assert.match(zeile, /role="alert"/, `${datei}: ${zeile}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in new issue
Block a user