From bc9b4d9576d1c36da6566877fb1dfacc81becdd8 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 14:10:34 +0000 Subject: [PATCH] Buchungskalender: Widget-Monatsnavigation und Tastaturfokus Co-Authored-By: Claude Opus 5.5 --- public/embed/unterkunft.js | 43 ++++++++++++++++++++---------- test/unterkunftWidgetFluss.test.js | 20 ++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/public/embed/unterkunft.js b/public/embed/unterkunft.js index a4c22c9..eee06c9 100644 --- a/public/embed/unterkunft.js +++ b/public/embed/unterkunft.js @@ -296,12 +296,17 @@ /* ---- Kalender ---- */ function monatKey(j, m) { return j * 12 + (m - 1) } + function zweiSichtbar() { return (root.offsetWidth || root.clientWidth || 0) >= 1100 } function aktualisiereZus() { if (sel.von && sel.bis) { zus.textContent = WKU_LOGIK.zusammenfassung(sel.von, sel.bis); hint.textContent = '' } else if (sel.von) { zus.textContent = 'Anreise ' + langesDatum(sel.von) + '. Bitte Abreise wählen.'; hint.textContent = hinweis } else { zus.textContent = ''; hint.textContent = '' } if (hinweis && !sel.bis) hint.textContent = hinweis } + function tageBisFrei(von, bis) { + for (var d = von; d < bis; d = plusTage(d, 1)) if (WKU_LOGIK.nachtBelegt(d, belegt)) return false + return true + } function tagKnopf(tag) { var p = zerlege(tag) var vergangen = tag < heute @@ -318,29 +323,29 @@ else if (istAn) kl.push('wku-an', 'wku-ab') if (istAb) kl.push('wku-ab') if (sel.von && sel.bis && tag > sel.von && tag < sel.bis) kl.push('wku-bereich') - var status = (nb ? 'belegt' : 'frei') + (istAn ? ', Anreise' : '') + (istAb ? ', Abreise' : '') + var status = (nb ? (sel.von && !sel.bis && tag > sel.von && !vergangen && !zu && !WKU_LOGIK.nachtBelegt(sel.von, belegt) && tageBisFrei(sel.von, tag) ? 'belegt – als Abreisetag möglich' : 'belegt') : 'frei') + (istAn ? ', Anreise' : '') + (istAb ? ', Abreise' : '') var b = h('button', { class: kl.join(' '), type: 'button', 'data-tag': tag, 'aria-label': langesDatum(tag) + ', ' + status, 'aria-pressed': (istAn || istAb) ? 'true' : 'false', tabindex: tag === fokusTag ? '0' : '-1' }, [h('span', { text: String(p[2]) })]) if (vergangen || zu) { b.setAttribute('aria-disabled', 'true') } b.addEventListener('click', function () { tagKlick(tag) }) return b } - function monatBauen(j, m, erster, letzter) { + function monatBauen(j, m, erster) { var kopf = h('div', { class: 'wku-monat-kopf' }) var min = monatKey(heute.slice(0, 4) * 1, heute.slice(5, 7) * 1) var max = monatKey(bisMax.slice(0, 4) * 1, bisMax.slice(5, 7) * 1) + var zur if (erster) { - var zur = h('button', { class: 'wku-pfeil', type: 'button', 'aria-label': 'Vorheriger Monat', text: '‹' }) + zur = h('button', { class: 'wku-pfeil', type: 'button', 'data-nav': 'zur', 'aria-label': 'Vorheriger Monat', text: '‹' }) if (monatKey(j, m) <= min) zur.setAttribute('disabled', '') zur.addEventListener('click', function () { blaettern(-1) }) kopf.appendChild(zur) } else kopf.appendChild(h('span', { class: 'wku-pfeil-leer' })) kopf.appendChild(h('h3', { class: 'wku-monat-name', text: MONAT_LANG[m - 1] + ' ' + j })) - if (letzter) { - var vor = h('button', { class: 'wku-pfeil' + (erster ? ' wku-weiter-ein' : ''), type: 'button', 'aria-label': 'Nächster Monat', text: '›' }) - if (monatKey(j, m) >= max) vor.setAttribute('disabled', '') - vor.addEventListener('click', function () { blaettern(1) }) - kopf.appendChild(vor) - } else kopf.appendChild(h('span', { class: 'wku-pfeil-leer wku-weiter-ein' })) + // Erster Monat: immer ein echter Weiter-Knopf (bei zwei sichtbaren Monaten per CSS ausgeblendet, dann sitzt er im zweiten Monat). + var vor = h('button', { class: 'wku-pfeil' + (erster ? ' wku-weiter-ein' : ''), type: 'button', 'data-nav': erster ? 'vor1' : 'vor2', 'aria-label': 'Nächster Monat', text: '›' }) + if (monatKey(j, m) >= max) vor.setAttribute('disabled', '') + vor.addEventListener('click', function () { blaettern(1) }) + kopf.appendChild(vor) var grid = h('div', { class: 'wku-raster', role: 'group', 'aria-label': MONAT_LANG[m - 1] + ' ' + j }) WOCHE_KURZ.forEach(function (w, i) { grid.appendChild(h('div', { class: 'wku-wt', 'aria-hidden': 'true', text: w, title: WOCHE_LANG[i] })) }) WKU_LOGIK.monatsRaster(j, m).forEach(function (z) { grid.appendChild(z.tag ? tagKnopf(z.tag) : h('div', { class: 'wku-leer' })) }) @@ -351,8 +356,8 @@ var a = ansicht var b = a.m === 12 ? { j: a.j + 1, m: 1 } : { j: a.j, m: a.m + 1 } monate.textContent = '' - monate.appendChild(monatBauen(a.j, a.m, true, false)) - monate.appendChild(monatBauen(b.j, b.m, false, true)) + monate.appendChild(monatBauen(a.j, a.m, true)) + monate.appendChild(monatBauen(b.j, b.m, false)) aktualisiereZus() if (fokus && fokusTag) { var k = monate.querySelector('[data-tag="' + fokusTag + '"]') @@ -367,7 +372,14 @@ var max = monatKey(bisMax.slice(0, 4) * 1, bisMax.slice(5, 7) * 1) if (monatKey(j, m) < min || monatKey(j, m) > max) return ansicht = { j: j, m: m } + var erster = iso(j, m, 1) + fokusTag = erster < heute ? heute : erster zeichne(false) + var zwei = zweiSichtbar() + var wunsch = d > 0 ? (zwei ? 'vor2' : 'vor1') : 'zur' + var knopf = monate.querySelector('[data-nav="' + wunsch + '"]') + if (!knopf || knopf.hasAttribute('disabled')) knopf = monate.querySelector(d > 0 ? '[data-nav="zur"]' : (zwei ? '[data-nav="vor2"]' : '[data-nav="vor1"]')) + if (knopf && knopf.focus) knopf.focus() } function tagKlick(tag) { if (tag < heute || tag > bisMax) return @@ -387,18 +399,21 @@ if (ziel < heute || ziel > bisMax) return fokusTag = ziel var p = zerlege(ziel) - var sichtbar = monatKey(p[0], p[1]) >= monatKey(ansicht.j, ansicht.m) && monatKey(p[0], p[1]) <= monatKey(ansicht.j, ansicht.m) + 1 + var sichtbar = monatKey(p[0], p[1]) >= monatKey(ansicht.j, ansicht.m) && monatKey(p[0], p[1]) <= monatKey(ansicht.j, ansicht.m) + (zweiSichtbar() ? 1 : 0) if (!sichtbar) ansicht = { j: p[0], m: p[1] } zeichne(true) }) function pad(n) { return zwei(n) } + var ladeNr = 0 function laden() { + var nr = (ladeNr += 1) var jetzt = new Date() var von = jetzt.getUTCFullYear() + '-' + pad(jetzt.getUTCMonth() + 1) + '-' + pad(jetzt.getUTCDate()) von = plusTage(von, -1) var bis = plusTage(von, 540) return holen(basis + '/belegung?unterkunft=' + encodeURIComponent(unterk.id) + '&von=' + von + '&bis=' + bis, { headers: { Accept: 'application/json' } }).then(function (r) { + if (nr !== ladeNr) return if (!r.ok || !r.body.heute || !r.body.bisMax) throw new Error('belegung') belegt = r.body.belegt || [] heute = r.body.heute @@ -439,12 +454,12 @@ formBox.textContent = '' formBox.appendChild(h('p', { class: 'wku-danke', role: 'status', tabindex: '-1', text: 'Danke! Ihre Anfrage ' + r.body.nummer + ' ist eingegangen. Sie erhalten gleich eine Bestätigung per E-Mail.' })) var d = formBox.querySelector('.wku-danke'); if (d && d.focus) d.focus() - ladenOderAusweichen() + laden().then(null, function () {}) return } var b = r.body || {} var gezeigt = false - if (b.felder) Object.keys(b.felder).forEach(function (k) { if (feldFehler(k, String(b.felder[k]))) gezeigt = true }) + if (b.felder) Object.keys(b.felder).forEach(function (k) { if (Object.prototype.hasOwnProperty.call(b.felder, k) && feldFehler(k, String(b.felder[k]))) gezeigt = true }) if (gezeigt) return if (b.fehler === 'belegt' || r.status === 409) { zeigeFehler('Dieser Zeitraum ist leider nicht mehr frei. Bitte wählen Sie einen anderen.'); laden().then(null, function () {}) } else if (r.status === 429) zeigeFehler('Zu viele Anfragen. Bitte versuchen Sie es später noch einmal.') diff --git a/test/unterkunftWidgetFluss.test.js b/test/unterkunftWidgetFluss.test.js index 2061560..47b84a5 100644 --- a/test/unterkunftWidgetFluss.test.js +++ b/test/unterkunftWidgetFluss.test.js @@ -70,3 +70,23 @@ test('Pfeiltasten bewegen den Fokus', async () => { assert.equal(fx.document.activeElement.getAttribute('data-tag'), '2026-11-18') } finally { fx.restore() } }) + +test('Monatsnavigation: erster Monat hat aktiven Weiter-Knopf, Fokus bleibt, Tab-Stop im gezeigten Monat, Abreisetag-Label', async () => { + const { fx, el } = start([{ status: 200, body: CFG }, { status: 200, body: BEL }]) + try { + await fx.ruhe(); await fx.ruhe() + const erster = fx.finde(el, '.wku-monat') + const weiter = fx.finde(erster, '[data-nav="vor1"]') + assert.ok(weiter && !weiter.hasAttribute('disabled')) + await fx.klick(weiter) + assert.match(fx.text(el), /Dezember 2026/) + assert.equal(fx.document.activeElement.getAttribute('data-nav'), 'vor1') + assert.equal(fx.finde(el, '[data-tag][tabindex="0"]').getAttribute('data-tag'), '2026-12-01') + await fx.klick(fx.finde(el, '[data-nav="zur"]')) + assert.equal(fx.document.activeElement.getAttribute('data-nav'), 'vor1') // Zurueck ist im ersten Monat gesperrt, Fokus wechselt zum Weiter-Knopf + assert.equal(fx.finde(el, '[data-tag][tabindex="0"]').getAttribute('data-tag'), '2026-11-01') + await fx.klick(tag(fx, el, '2026-11-18')) + assert.match(tag(fx, el, '2026-11-20').getAttribute('aria-label'), /belegt – als Abreisetag möglich/) + assert.doesNotMatch(tag(fx, el, '2026-11-22').getAttribute('aria-label'), /Abreisetag möglich/) + } finally { fx.restore() } +})