Terminplaner: Portal-Ansicht – Fokus und aria-busy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ad056f261
commit
4be886a4de
3 files changed
+85
-21
No files matched your search
@@ -1,6 +1,6 @@
|
||||
/* Terminplaner im Kundenbereich: Speichern der Einstellungen – gemeinsam fuer die Reiter Leistungen, Team & Plaetze und Einstellungen.
|
||||
Jede Aenderung ist ein Speichern: PATCH /api/termine/einstellungen schickt die GANZE Einstellung mit der Version, auf der sie beruht
|
||||
(server/routes/termine.js). Der Reiter gibt nur an, was sich aendert: speichere(aendere, { container, knopf }) holt eine Kopie des
|
||||
(server/routes/termine.js). Der Reiter gibt nur an, was sich aendert: speichere(aendere, { container }) holt eine Kopie des
|
||||
aktuellen Standes (ctx.daten.einstellung), laesst aendere(entwurf) sie aendern und schickt sie ab.
|
||||
- Erfolg: die zurueckgegebene Einstellung und Version gelten (ctx.daten), die Hinweisleiste der Huelle wird neu gezeichnet und die Daten
|
||||
(Kalender-Links) werden neu geladen; Rueckgabe true.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
import { allgemeinerFehler, frage, h, knopfZeile, setzeFehler, zeigeFehler } from './termine-dom.js'
|
||||
|
||||
/** Wie viele Speichern gerade auf einem Tabpanel laufen (aria-busy bleibt, bis das letzte fertig ist). */
|
||||
const belegt = new WeakMap()
|
||||
|
||||
export const VERALTET_TEXT = 'Die Einstellungen wurden inzwischen geändert. Bitte neu laden.'
|
||||
|
||||
const WORT = { leistungen: 'Leistung', abwesenheiten: 'Abwesenheit' }
|
||||
@@ -62,18 +65,23 @@ export function neuerSpeicher(ctx, wurzel, neuZeichnen) {
|
||||
}
|
||||
})
|
||||
|
||||
// Ein Speichern zur Zeit: waehrenddessen aria-busy am Reiter und alle Knoepfe darin gesperrt (danach wieder frei, der Fokus kehrt zurueck)
|
||||
// Ein Speichern zur Zeit: waehrenddessen aria-busy am Reiter und alle Knoepfe darin gesperrt (danach wieder frei, der Fokus kehrt zurueck).
|
||||
// Das Tabpanel ist fuer alle Reiter dasselbe Element und wechselt der Nutzer waehrend des Speicherns den Reiter, ist wurzel beim Freigeben
|
||||
// schon ausgehaengt: darum merkt sich die Sperre das Panel, das sie gesetzt hat, und aria-busy faellt erst, wenn kein Speichern mehr darauf laeuft.
|
||||
let laeuft = false
|
||||
function sperre(an, gesperrt) {
|
||||
const tafel = wurzel.closest('[role="tabpanel"]') || wurzel
|
||||
if (an) {
|
||||
const tafel = wurzel.closest('[role="tabpanel"]') || wurzel
|
||||
belegt.set(tafel, (belegt.get(tafel) || 0) + 1)
|
||||
tafel.setAttribute('aria-busy', 'true')
|
||||
const knoepfe = [...wurzel.querySelectorAll('button:not(:disabled)')]
|
||||
const fokus = document.activeElement
|
||||
for (const knopf of knoepfe) knopf.disabled = true
|
||||
return { knoepfe, fokus }
|
||||
return { tafel, knoepfe, fokus }
|
||||
}
|
||||
tafel.removeAttribute('aria-busy')
|
||||
const rest = (belegt.get(gesperrt.tafel) || 1) - 1
|
||||
belegt.set(gesperrt.tafel, rest)
|
||||
if (rest === 0) gesperrt.tafel.removeAttribute('aria-busy')
|
||||
for (const knopf of gesperrt.knoepfe) knopf.disabled = false
|
||||
// Der Fokus war auf einem gesperrten Knopf und ist weg: zurueck dorthin; hat der Reiter ihn inzwischen anders gesetzt (z. B. auf ein Feld mit Fehler), bleibt das so
|
||||
const jetzt = document.activeElement
|
||||
|
||||
+23
-16
@@ -188,22 +188,29 @@ function kalenderLink(t, r) {
|
||||
allgemeinerFehler(),
|
||||
zeile)
|
||||
// exklusiv wie ein Speichern: das Erzeugen aendert die Version, ein gleichzeitiges Speichern bekaeme sonst die alte
|
||||
box.addEventListener('submit', () => t.sp.exklusiv(async () => {
|
||||
try {
|
||||
const antwort = await ctx.api.send('POST', `/ressourcen/${encodeURIComponent(r.id)}/feed`)
|
||||
if (!ctx.aktuell() || !eingabe.isConnected) return
|
||||
ctx.daten.version = antwort.version // das Erzeugen zaehlt die Version hoch: das naechste Speichern baut darauf auf
|
||||
ctx.daten.feeds = { ...ctx.daten.feeds, [r.id]: antwort.url }
|
||||
eingabe.value = antwort.url
|
||||
frageBox.replaceChildren()
|
||||
status.textContent = 'Der neue Link ist da. Der alte funktioniert nicht mehr.'
|
||||
ctx.meldung(`Neuer Kalender-Link für „${r.name}“ erzeugt.`)
|
||||
ausloeser.focus()
|
||||
} catch (err) {
|
||||
if (!ctx.aktuell() || !eingabe.isConnected) return
|
||||
zeigeFehler(box, err)
|
||||
}
|
||||
}))
|
||||
box.addEventListener('submit', async () => {
|
||||
const erzeugt = await t.sp.exklusiv(async () => {
|
||||
try {
|
||||
const antwort = await ctx.api.send('POST', `/ressourcen/${encodeURIComponent(r.id)}/feed`)
|
||||
if (!ctx.aktuell()) return false
|
||||
ctx.daten.version = antwort.version // das Erzeugen zaehlt die Version hoch: das naechste Speichern baut darauf auf
|
||||
ctx.daten.feeds = { ...ctx.daten.feeds, [r.id]: antwort.url }
|
||||
if (!eingabe.isConnected) return true // der Reiter wurde inzwischen neu gezeichnet und zeigt schon den neuen Link
|
||||
eingabe.value = antwort.url
|
||||
frageBox.replaceChildren()
|
||||
status.textContent = 'Der neue Link ist da. Der alte funktioniert nicht mehr.'
|
||||
ctx.meldung(`Neuer Kalender-Link für „${r.name}“ erzeugt.`)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (ctx.aktuell() && eingabe.isConnected) zeigeFehler(box, err)
|
||||
return false
|
||||
}
|
||||
})
|
||||
// Fokus erst nach dem Freigeben der Knoepfe (im gesperrten Zustand nimmt ein Knopf keinen Fokus an); ersetzt, den Knopf derselben Person suchen
|
||||
if (!erzeugt || !t.sp.nochHier()) return
|
||||
const knopf = ausloeser.isConnected ? ausloeser : t.wurzel.querySelector(`li[data-id="${r.id}"] [data-aktion="link-neu"]`)
|
||||
knopf?.focus()
|
||||
})
|
||||
frageBox.replaceChildren(box)
|
||||
box.focus()
|
||||
}
|
||||
|
||||
@@ -1590,6 +1590,55 @@ async function pruefungen() {
|
||||
eq([auswahl.von, auswahl.bis], [0, auswahl.laenge], 'der ganze Link ist markiert')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Fokus nach „Neuen Link erzeugen“
|
||||
await check('echt_feed_link_fokus_nach_erzeugen', async (page) => {
|
||||
await oeffneReiter(page, 'team')
|
||||
await knopf(zeileMitId(page, R_ANNA), 'Bearbeiten: Anna').click()
|
||||
const feed = zeileMitId(page, R_ANNA).locator('.tm-feed')
|
||||
const link = feed.locator('.tm-link-feld')
|
||||
const vorher = await link.inputValue()
|
||||
await knopf(feed, 'Neuen Link erzeugen').click()
|
||||
await knopf(page.getByRole('form', { name: 'Neuen Kalender-Link erzeugen?' }), 'Neuen Link erzeugen').click()
|
||||
await page.waitForFunction((alt) => document.querySelector('#tm-panel .tm-link-feld')?.value !== alt, vorher)
|
||||
await page.waitForFunction(() => !document.querySelector('#tm-panel').hasAttribute('aria-busy'))
|
||||
// der Fokus steht wieder auf dem Knopf, der die Rueckfrage geoeffnet hat (und der ist frei), nicht verloren auf der Seite
|
||||
const aktiv = await page.evaluate(() => ({ aktion: document.activeElement?.dataset?.aktion, gesperrt: document.activeElement?.disabled, text: document.activeElement?.textContent }))
|
||||
eq(aktiv, { aktion: 'link-neu', gesperrt: false, text: 'Neuen Link erzeugen' }, 'Fokus nach dem Erzeugen')
|
||||
// ... und per Tastatur geht es weiter: Enter oeffnet die naechste Rueckfrage
|
||||
await page.keyboard.press('Enter')
|
||||
await page.getByRole('form', { name: 'Neuen Kalender-Link erzeugen?' }).waitFor()
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Reiter wechseln, waehrend ein Speichern laeuft: aria-busy faellt
|
||||
await check('echt_busy_nach_reiterwechsel', async (page) => {
|
||||
// die Antwort auf ein Speichern braucht 0,9 s
|
||||
await page.route('**/api/termine/einstellungen', async (route) => {
|
||||
if (route.request().method() === 'PATCH') await new Promise((fertig) => { setTimeout(fertig, 900) })
|
||||
await route.fallback()
|
||||
})
|
||||
await oeffneReiter(page, 'leistungen')
|
||||
const v0 = version()
|
||||
await knopf(zeileMitId(page, S_HERREN), 'Bearbeiten: Herrenschnitt').click()
|
||||
const form = formular(page, 'Leistung bearbeiten: Herrenschnitt')
|
||||
await form.getByLabel('Dauer (Minuten)', { exact: true }).fill('35')
|
||||
await knopf(form, 'Speichern').click()
|
||||
await page.locator('#tm-panel[aria-busy="true"]').waitFor()
|
||||
// mitten im Speichern zu Team & Plaetze wechseln: der neue Reiter hat sein eigenes, freies Gerueest
|
||||
await page.locator('[data-reiter="team"]').click()
|
||||
await page.locator('#tm-tab-team[aria-selected="true"]').waitFor()
|
||||
await page.locator('#tm-panel li[data-id="r_anna0001"]').waitFor()
|
||||
// das Speichern geht zu Ende, danach darf aria-busy nicht stehen bleiben
|
||||
await warteBis(() => version() === v0 + 1, 'Speichern angekommen')
|
||||
await page.waitForFunction(() => !document.querySelector('#tm-panel').hasAttribute('aria-busy'), null, { timeout: 4000 })
|
||||
eq(await page.locator('#tm-panel[aria-busy]').count(), 0, 'aria-busy nach dem Speichern weg')
|
||||
eq(await page.locator('#tm-panel .tm-tab button:disabled').count() > 0, false, 'die Knöpfe des neuen Reiters sind frei')
|
||||
eq(await knopf(page, '+ Person oder Platz hinzufügen').isEnabled(), true, 'neuer Reiter bedienbar')
|
||||
// das Ergebnis des Speicherns ist trotzdem da und der Reiter Leistungen zeigt es
|
||||
await page.locator('[data-reiter="leistungen"]').click()
|
||||
assert((await textVon(zeileMitId(page, S_HERREN).locator('.tm-meta').first())).includes('35 Min.'), 'Leistungen zeigen die gespeicherte Dauer')
|
||||
eq(await page.locator('#tm-panel[aria-busy]').count(), 0, 'weiter kein aria-busy')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------ Konflikt mit zweitem Tab
|
||||
await check('echt_konflikt_zweiter_tab', async (page, ctx) => {
|
||||
await oeffneReiter(page, 'einstellungen')
|
||||
|
||||
Reference in new issue
Block a user