diff --git a/distro-packages/Arch-Linux/skald-git/PKGBUILD b/distro-packages/Arch-Linux/skald-git/PKGBUILD index 0a29284..aaff88a 100644 --- a/distro-packages/Arch-Linux/skald-git/PKGBUILD +++ b/distro-packages/Arch-Linux/skald-git/PKGBUILD @@ -2,7 +2,7 @@ # shellcheck shell=bash disable=SC2034,SC2154 pkgname=skald-git -pkgver=0.0.0.r1537.g7cfbcda +pkgver=0.0.0.r1538.g8e265ed pkgrel=1 pkgdesc='Audio-only hall-based conferencing server' arch=('x86_64' 'aarch64') diff --git a/static/skald.js b/static/skald.js index d293151..656ccc9 100644 --- a/static/skald.js +++ b/static/skald.js @@ -2589,6 +2589,10 @@ function formatTime(time) { /** @type {lastMessage} */ let lastMessage = {}; +/** @type {Object.} */ +let liveRegionStates = {}; +let liveRegionDelay = 700; + /** * @param {string} text * @param {string} elementId @@ -2598,9 +2602,42 @@ function announceLiveRegion(text, elementId) { if(!announcement) return; - announcement.textContent = ''; + let state = liveRegionStates[elementId]; + if(!state) { + state = {queue: [], active: false}; + liveRegionStates[elementId] = state; + } + + state.queue.push(String(text)); + if(!state.active) + flushLiveRegion(elementId); +} + +/** + * @param {string} elementId + */ +function flushLiveRegion(elementId) { + let state = liveRegionStates[elementId]; + let announcement = document.getElementById(elementId); + if(!state || !announcement) { + if(state) + state.active = false; + return; + } + + let text = state.queue.shift(); + if(text === undefined) { + state.active = false; + return; + } + + state.active = true; + announcement.replaceChildren(); window.setTimeout(() => { - announcement.textContent = text; + let item = document.createElement('div'); + item.textContent = text; + announcement.appendChild(item); + window.setTimeout(() => flushLiveRegion(elementId), liveRegionDelay); }, 20); } diff --git a/webserver/static_accessibility_test.go b/webserver/static_accessibility_test.go new file mode 100644 index 0000000..ae8cd93 --- /dev/null +++ b/webserver/static_accessibility_test.go @@ -0,0 +1,62 @@ +package webserver + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func readStaticFile(t *testing.T, name string) string { + t.Helper() + data, err := os.ReadFile(filepath.Join("..", "static", name)) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + return string(data) +} + +func requireContains(t *testing.T, text, want, context string) { + t.Helper() + if !strings.Contains(text, want) { + t.Fatalf("%s missing %q", context, want) + } +} + +func requireFunctionContains(t *testing.T, source, functionName, want string) { + t.Helper() + start := strings.Index(source, "function "+functionName+"(") + if start < 0 { + t.Fatalf("missing function %s", functionName) + } + next := strings.Index(source[start+1:], "\nfunction ") + end := len(source) + if next >= 0 { + end = start + 1 + next + } + body := source[start:end] + if !strings.Contains(body, want) { + t.Fatalf("function %s missing %q", functionName, want) + } +} + +func TestStaticLiveRegionAnnouncementsStayWired(t *testing.T) { + html := readStaticFile(t, "skald.html") + js := readStaticFile(t, "skald.js") + + requireContains(t, html, `id="chat-announcements"`, "chat live region") + requireContains(t, html, `role="status"`, "chat live region") + requireContains(t, html, `aria-live="polite"`, "chat live region") + requireContains(t, html, `id="urgent-announcements"`, "urgent live region") + requireContains(t, html, `role="alert"`, "urgent live region") + requireContains(t, html, `aria-live="assertive"`, "urgent live region") + + requireFunctionContains(t, js, "announceChat", "chat-announcements") + requireFunctionContains(t, js, "announceUrgent", "urgent-announcements") + requireFunctionContains(t, js, "announceLiveRegion", "state.queue.push") + requireFunctionContains(t, js, "announceLiveRegion", "flushLiveRegion") + requireFunctionContains(t, js, "flushLiveRegion", "appendChild") + requireFunctionContains(t, js, "addToChatbox", "announceChat") + requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand raised`)") + requireFunctionContains(t, js, "setUserStatus", "announceUrgent(`${username}: hand lowered`)") +}