feat(ui): focus trap for consent dialog + recording-state live region (T7.5, FR-UX-4)

Traps focus in the recording-consent overlay the same way as Settings.
Adds a visually-hidden aria-live region announcing "Recording
started/paused/stopped" — previously nothing in the header was a live
region, so a non-visual user had no way to learn recording had begun.
This commit is contained in:
iamdoubz
2026-07-02 07:31:13 -05:00
parent d94472f476
commit 5f8ba8253e
+38 -1
View File
@@ -7,6 +7,7 @@
import Settings from "./lib/views/Settings.svelte";
import ConsentNotice from "./lib/components/ConsentNotice.svelte";
import LevelMeter from "./lib/components/LevelMeter.svelte";
import { trapFocus } from "./lib/actions/trapFocus";
import { recording } from "./lib/stores/recording.svelte";
import { settings } from "./lib/stores/settings.svelte";
import { meetings } from "./lib/stores/meetings.svelte";
@@ -64,6 +65,23 @@
);
}
// Screen-reader announcement for recording state changes (T7.5, FR-UX-4) —
// nothing else in the header is a live region, so this is the only way a
// non-visual user learns recording started/stopped. Tracked via an effect
// (not a plain $derived) so "stopped" fires only after having recorded,
// never on initial idle load.
let recordingAnnouncement = $state("");
let previousRecordingState = recording.state;
$effect(() => {
const current = recording.state;
if (current !== previousRecordingState) {
if (current === "recording") recordingAnnouncement = "Recording started";
else if (current === "paused") recordingAnnouncement = "Recording paused";
else if (previousRecordingState !== "idle") recordingAnnouncement = "Recording stopped";
previousRecordingState = current;
}
});
function onGlobalKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
if (showConsent) showConsent = false;
@@ -100,6 +118,7 @@
</script>
<div class="app" data-theme={resolvedTheme}>
<div class="sr-only" role="status" aria-live="polite">{recordingAnnouncement}</div>
<header class="bar">
<strong>WhispAssist</strong>
<span class="muted">local · private</span>
@@ -152,7 +171,14 @@
</header>
{#if showConsent}
<div class="consent-overlay" role="dialog" aria-modal="true" aria-label="Recording consent">
<div
class="consent-overlay"
role="dialog"
aria-modal="true"
aria-label="Recording consent"
tabindex="-1"
use:trapFocus
>
<ConsentNotice onAccept={acceptConsentAndRecord} onCancel={() => (showConsent = false)} />
</div>
{/if}
@@ -169,6 +195,17 @@
</div>
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
:global(:root) {
--bg: #ffffff;
--fg: #1a1a1a;