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:
+38
-1
@@ -7,6 +7,7 @@
|
|||||||
import Settings from "./lib/views/Settings.svelte";
|
import Settings from "./lib/views/Settings.svelte";
|
||||||
import ConsentNotice from "./lib/components/ConsentNotice.svelte";
|
import ConsentNotice from "./lib/components/ConsentNotice.svelte";
|
||||||
import LevelMeter from "./lib/components/LevelMeter.svelte";
|
import LevelMeter from "./lib/components/LevelMeter.svelte";
|
||||||
|
import { trapFocus } from "./lib/actions/trapFocus";
|
||||||
import { recording } from "./lib/stores/recording.svelte";
|
import { recording } from "./lib/stores/recording.svelte";
|
||||||
import { settings } from "./lib/stores/settings.svelte";
|
import { settings } from "./lib/stores/settings.svelte";
|
||||||
import { meetings } from "./lib/stores/meetings.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) {
|
function onGlobalKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
if (showConsent) showConsent = false;
|
if (showConsent) showConsent = false;
|
||||||
@@ -100,6 +118,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app" data-theme={resolvedTheme}>
|
<div class="app" data-theme={resolvedTheme}>
|
||||||
|
<div class="sr-only" role="status" aria-live="polite">{recordingAnnouncement}</div>
|
||||||
<header class="bar">
|
<header class="bar">
|
||||||
<strong>WhispAssist</strong>
|
<strong>WhispAssist</strong>
|
||||||
<span class="muted">local · private</span>
|
<span class="muted">local · private</span>
|
||||||
@@ -152,7 +171,14 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if showConsent}
|
{#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)} />
|
<ConsentNotice onAccept={acceptConsentAndRecord} onCancel={() => (showConsent = false)} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -169,6 +195,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<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) {
|
:global(:root) {
|
||||||
--bg: #ffffff;
|
--bg: #ffffff;
|
||||||
--fg: #1a1a1a;
|
--fg: #1a1a1a;
|
||||||
|
|||||||
Reference in New Issue
Block a user