Kundenansicht neu (Aufträge): Zeitleiste barrierefrei, "mehr anzeigen" mit Auftragstitel im Namen und aria-controls, "Im Chat antworten" bricht nicht um
Der Knopf "mehr anzeigen"/"weniger anzeigen" bekommt einen aria-label mit dem Auftragstitel (der sichtbare Text steht vorn im Namen, der Titel nur als Text) und aria-controls auf die Liste seines Auftrags (eindeutige Id je Liste); beides zieht beim Umschalten mit. Der Linktext "Im Chat antworten" trägt white-space: nowrap und bleibt in einer Zeile beisammen. Tests: DOM-Test für Name, Verknüpfung, Umschalten, fremden Titel und Ersatztitel; CSS-Test für nowrap. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
14dc2ad1bf
commit
ace4c85e72
3 files changed
+55
-8
No files matched your search
@@ -120,7 +120,7 @@
|
||||
.kn-az-kopf .kn-pille { flex: none; margin-top: 2px; }
|
||||
.kn-az-info { margin: 8px 0 0; font-size: 14px; line-height: 20px; }
|
||||
.kn-az-info a { color: var(--kn-orange-fg); text-decoration: none; }
|
||||
.kn-az-info-link { font-weight: 600; text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 3px; }
|
||||
.kn-az-info-link { font-weight: 600; text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 3px; white-space: nowrap; } /* der Linktext bleibt zusammen */
|
||||
.kn-az-balken { display: flex; gap: 3px; margin: 12px 0 0; padding: 0; list-style: none; }
|
||||
.kn-az-balken li { position: relative; flex: 1; height: 4px; }
|
||||
.kn-az-balken li::before { content: ""; position: absolute; inset: 0; border-radius: 2px; background: var(--kn-line); } /* die Fläche liegt im Pseudo-Element: der Lesetext darunter bleibt auf Weiß */
|
||||
|
||||
@@ -122,12 +122,15 @@ const KEINE_EINTRAEGE = 'Noch keine Arbeiten dokumentiert.'
|
||||
|
||||
/** Knopf, der den nächsten Wrapper [data-einklappbar] auf- und zuklappt (is-open); die Zeilen dahinter tragen [data-extra]. Die CSS-Regeln dazu gelten nur für
|
||||
direkte Kinder (ansichten.css, .kn-az-…): die allgemeine Regel .kn-einklappbar würde bei verschachtelten Wrappern auch offene Zeilen verbergen. */
|
||||
function umschalter({ attribut, klasse, mehr, weniger }) {
|
||||
const knopf = h('button', { type: 'button', class: klasse, [attribut]: true, 'aria-expanded': 'false' }, mehr)
|
||||
function umschalter({ attribut, klasse, mehr, weniger, name, steuert }) {
|
||||
// name: Zusatz für den Namen des Knopfes („mehr anzeigen zu „Titel““, der sichtbare Text steht vorn im Namen); steuert: Id der Liste, die er auf- und zuklappt
|
||||
const nameFuer = (text) => (name ? `${text} zu „${name}“` : null)
|
||||
const knopf = h('button', { type: 'button', class: klasse, [attribut]: true, 'aria-expanded': 'false', 'aria-label': nameFuer(mehr), 'aria-controls': steuert || null }, mehr)
|
||||
knopf.addEventListener('click', () => {
|
||||
const offen = knopf.closest('[data-einklappbar]').classList.toggle('is-open')
|
||||
knopf.textContent = offen ? weniger : mehr
|
||||
knopf.setAttribute('aria-expanded', String(offen))
|
||||
if (name) knopf.setAttribute('aria-label', nameFuer(offen ? weniger : mehr))
|
||||
})
|
||||
return knopf
|
||||
}
|
||||
@@ -143,28 +146,34 @@ function baueBalken(status) {
|
||||
h('span', { class: 'kn-nur-lesen' }, `${stufe.name}, ${zustandText(stufe)}`))))
|
||||
}
|
||||
|
||||
function baueZeitleiste(zeilen) {
|
||||
let listenNummer = 0 // Ids der Listen für aria-controls: eindeutig je Seite, auch wenn die Ansicht mehrmals gezeichnet wird
|
||||
|
||||
function baueZeitleiste(zeilen, titel) {
|
||||
const { sichtbar, rest } = kuerze(zeilen, MAX_ZEILEN)
|
||||
listenNummer += 1
|
||||
const listenId = `kn-az-zeilen-${listenNummer}`
|
||||
const zeile = (z, extra) => h('li', { 'data-extra': extra },
|
||||
z.datum ? h('time', { class: 'kn-az-datum', datetime: z.iso || null }, z.datum) : h('span', { class: 'kn-az-datum', 'aria-hidden': 'true' }),
|
||||
h('span', { class: 'kn-az-text' }, z.text))
|
||||
return h('div', { class: 'kn-az-zeitleiste', 'data-einklappbar': true },
|
||||
// Trägt ein Datum das Jahr (TT.MM.JJ), bekommt die Datumsspalte aller Zeilen die Breite dafür, damit die Texte bündig bleiben
|
||||
h('ol', { class: zeilen.some((z) => z.datum.length > 6) ? 'kn-az-zeilen kn-az-mit-jahr' : 'kn-az-zeilen' }, sichtbar.map((z) => zeile(z, false)), rest.map((z) => zeile(z, true))),
|
||||
rest.length ? umschalter({ attribut: 'data-mehr', klasse: 'kn-textknopf kn-az-mehr', mehr: 'mehr anzeigen', weniger: 'weniger anzeigen' }) : null)
|
||||
h('ol', { id: listenId, class: zeilen.some((z) => z.datum.length > 6) ? 'kn-az-zeilen kn-az-mit-jahr' : 'kn-az-zeilen' }, sichtbar.map((z) => zeile(z, false)), rest.map((z) => zeile(z, true))),
|
||||
// Mehrere Knöpfe „mehr anzeigen“ auf einer Seite: der Name nennt den Auftrag, aria-controls die Liste
|
||||
rest.length ? umschalter({ attribut: 'data-mehr', klasse: 'kn-textknopf kn-az-mehr', mehr: 'mehr anzeigen', weniger: 'weniger anzeigen', name: titel, steuert: listenId }) : null)
|
||||
}
|
||||
|
||||
function baueAuftragsBlock(auftrag, jetzt, extra) {
|
||||
const status = auftrag.status && typeof auftrag.status === 'object' ? auftrag.status : {}
|
||||
const zeilen = zeitleistenZeilen(auftrag, jetzt)
|
||||
const titel = auftrag.topic || 'Dein Auftrag'
|
||||
return h('li', { class: 'kn-az', 'data-auftrag': auftrag.number || null, 'data-extra': extra },
|
||||
h('div', { class: 'kn-az-kopf' },
|
||||
h('h3', { class: 'kn-az-titel' }, auftrag.topic || 'Dein Auftrag'),
|
||||
h('h3', { class: 'kn-az-titel' }, titel),
|
||||
status.label ? h('span', { class: K.pille[statusTon(status.tone)] }, status.label) : null),
|
||||
// Die ganze Zeile führt in den Chat (große Tippfläche am Handy); unterstrichen ist nur „Im Chat antworten“
|
||||
braucheInfo(auftrag) ? h('p', { class: 'kn-az-info' }, h('a', zuAnsicht('chat'), 'Wir brauchen eine Info von dir · ', h('span', { class: 'kn-az-info-link' }, 'Im Chat antworten'))) : null,
|
||||
baueBalken(status),
|
||||
zeilen.length ? baueZeitleiste(zeilen) : h('p', { class: 'kn-az-leer' }, KEINE_EINTRAEGE))
|
||||
zeilen.length ? baueZeitleiste(zeilen, titel) : h('p', { class: 'kn-az-leer' }, KEINE_EINTRAEGE))
|
||||
}
|
||||
|
||||
/** „Was wir zuletzt erledigt haben“: ein Block je Auftrag; fünf offen, der Rest hinter „Ältere Aufträge anzeigen“. Ohne Aufträge ein ruhiger Satz. */
|
||||
|
||||
@@ -99,6 +99,37 @@ test('Aktivität: Zeitleiste mit Datum und Kurztext, drei sichtbar, „mehr anze
|
||||
assert.equal(fx.alle(el, '[data-mehr]').length, 1)
|
||||
}))
|
||||
|
||||
// Barrierefreiheit: Mehrere Knöpfe „mehr anzeigen“ auf einer Seite sind für Screenreader nicht zu unterscheiden. Der Name nennt darum den Auftrag (und enthält den sichtbaren
|
||||
// Text, WCAG 2.5.3), aria-controls zeigt auf die Liste, die der Knopf aufklappt.
|
||||
test('Zeitleiste: „mehr anzeigen“ trägt aria-label mit dem Auftragstitel und aria-controls auf die eigene Liste, auch nach dem Umschalten', () => mitDom((fx) => {
|
||||
const vier = (nr, titel) => auftrag(nr, 'arbeit', '2026-10-06T10:00:00Z', ['a', 'b', 'c', 'd'].map((t, j) => eintrag(`2026-10-1${j}`, t)), titel)
|
||||
const el = baueAktivitaet([vier(1, 'Speisekarte als PDF'), vier(2, 'Neue Fotos <b>für</b> die Startseite')], JETZT)
|
||||
const [erster, zweiter] = fx.alle(el, '[data-auftrag]')
|
||||
const knopf = (block) => fx.finde(block, '[data-mehr]')
|
||||
const liste = (block) => fx.finde(block, '.kn-az-zeilen')
|
||||
assert.equal(knopf(erster).textContent, 'mehr anzeigen')
|
||||
assert.equal(knopf(erster).getAttribute('aria-label'), 'mehr anzeigen zu „Speisekarte als PDF“')
|
||||
assert.equal(knopf(zweiter).getAttribute('aria-label'), 'mehr anzeigen zu „Neue Fotos <b>für</b> die Startseite“') // nur Text, nie HTML
|
||||
// aria-controls: die Id der Liste desselben Auftrags, eindeutig auf der Seite
|
||||
const ids = [liste(erster).getAttribute('id'), liste(zweiter).getAttribute('id')]
|
||||
assert.ok(ids.every((id) => /^[A-Za-z][\w-]*$/.test(id)) && ids[0] !== ids[1], ids.join())
|
||||
assert.deepEqual([knopf(erster), knopf(zweiter)].map((k) => k.getAttribute('aria-controls')), ids)
|
||||
assert.ok(knopf(erster).getAttribute('aria-label').startsWith(knopf(erster).textContent)) // der sichtbare Text steht im Namen
|
||||
// Umschalten: Text, Zustand und Name ziehen mit, die Verknüpfung bleibt
|
||||
fx.klick(knopf(erster))
|
||||
assert.equal(knopf(erster).textContent, 'weniger anzeigen')
|
||||
assert.equal(knopf(erster).getAttribute('aria-label'), 'weniger anzeigen zu „Speisekarte als PDF“')
|
||||
assert.equal(knopf(erster).getAttribute('aria-controls'), ids[0])
|
||||
assert.equal(knopf(zweiter).getAttribute('aria-label'), 'mehr anzeigen zu „Neue Fotos <b>für</b> die Startseite“') // der andere Knopf bleibt
|
||||
fx.klick(knopf(erster))
|
||||
assert.equal(knopf(erster).getAttribute('aria-label'), 'mehr anzeigen zu „Speisekarte als PDF“')
|
||||
// ohne Titel gilt der Ersatztitel der Überschrift; „Ältere Aufträge anzeigen“ bleibt wie es war
|
||||
const ohneTitel = baueAktivitaet([{ ...vier(3, ''), topic: '' }], JETZT)
|
||||
assert.equal(fx.finde(ohneTitel, '[data-mehr]').getAttribute('aria-label'), 'mehr anzeigen zu „Dein Auftrag“')
|
||||
const sechs = baueAktivitaet(Array.from({ length: 6 }, (_, i) => vier(10 + i, `T${i}`)), JETZT)
|
||||
assert.equal(fx.finde(sechs, '[data-aeltere]').getAttribute('aria-label'), null)
|
||||
}))
|
||||
|
||||
test('Aktivität: Auftrag ohne Einträge zeigt einen ruhigen Satz statt einer leeren Zeitleiste', () => mitDom((fx) => {
|
||||
const el = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z', [])], JETZT)
|
||||
assert.equal(fx.finde(el, '.kn-az-zeitleiste'), null); assert.match(fx.text(el), /Noch keine Arbeiten dokumentiert\./)
|
||||
@@ -175,6 +206,13 @@ test('Rückfrage-Link: am Handy ein Block (kein flex) mit Innenabstand für 44 p
|
||||
assert.doesNotMatch(css, /\.kn-az-info a \{[^}]*display: *(inline-)?flex/)
|
||||
})
|
||||
|
||||
// Browserlauf 17.10. (Prüfbefund): „Im Chat antworten“ brach am schmalen Handy mitten im Text um. Der Linktext bleibt zusammen; passt er nicht mehr in die Zeile, wandert er ganz in die nächste.
|
||||
test('Rückfrage-Link: der Linktext „Im Chat antworten“ bricht nicht um (white-space: nowrap auf .kn-az-info-link)', () => {
|
||||
const css = readFileSync(new URL('../public/neu/ansichten.css', import.meta.url), 'utf8')
|
||||
assert.match(css, /\n\.kn-az-info-link \{[^}]*\bwhite-space: nowrap;[^}]*\}/)
|
||||
assert.doesNotMatch(css, /\.kn-az-info a \{[^}]*white-space: nowrap/) // der Satz davor darf umbrechen
|
||||
})
|
||||
|
||||
test('Zeitleiste: trägt ein Eintrag das Jahr (Vorjahr), richten sich alle Datumsspalten des Auftrags danach', () => mitDom((fx) => {
|
||||
const mitJahr = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z', [eintrag('2026-10-06', 'neu'), eintrag('2025-12-24', 'alt')])], JETZT)
|
||||
assert.deepEqual(fx.alle(mitJahr, 'time').map((t) => t.textContent), ['06.10.', '24.12.25']); assert.ok(fx.finde(mitJahr, '.kn-az-zeilen').classList.contains('kn-az-mit-jahr'))
|
||||
|
||||
Reference in new issue
Block a user