a11y: screen reader and keyboard improvements

- Audio player: aria-labels for Play/Pause, Loop, Mute, Close; labelled playback and volume sliders
- Generation: aria-labels for Generate speech and Fine tune instructions buttons
- Voice cards: focusable, labelled, Enter/Space to select
- History rows: focusable, labelled, Enter/Space to play; transcript textarea labelled
- Voices tab: focusable rows, labelled, Enter/Space to edit; Actions button labelled
- Model management: focusable model rows and labelled Download/Delete buttons
- Server tab: regions with aria-label and tabIndex for Connection, Status, App Updates
- Stories: focusable story rows, labelled, Enter/Space to select; Actions and track editor buttons labelled
- Voice profile samples: Play/Pause/Stop and mini-player slider labelled

Tested with NVDA and Narrator on Windows. See docs/PR-ACCESSIBILITY.md for full description.

Made-with: Cursor
This commit is contained in:
Richard Orme
2026-03-07 12:02:33 -08:00
parent 38bf96ff20
commit 19a28bf6c5
16 changed files with 256 additions and 23 deletions
@@ -278,9 +278,25 @@ interface ModelItemProps {
function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: ModelItemProps) {
// Use server's downloading state OR local state (for immediate feedback before server updates)
const showDownloading = model.downloading || isDownloading;
const statusText = model.loaded
? 'Loaded'
: showDownloading
? 'Downloading'
: model.downloaded
? 'Downloaded'
: 'Not downloaded';
const sizeText =
model.downloaded && model.size_mb && !showDownloading ? `, ${formatSize(model.size_mb)}` : '';
const rowLabel = `${model.display_name}, ${statusText}${sizeText}. Use Tab to reach Download or Delete.`;
return (
<div className="flex items-center justify-between p-3 border rounded-lg">
<div
className="flex items-center justify-between p-3 border rounded-lg"
role="group"
tabIndex={0}
aria-label={rowLabel}
>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">{model.display_name}</span>
@@ -314,17 +330,27 @@ function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: M
variant="outline"
disabled={model.loaded}
title={model.loaded ? 'Unload model before deleting' : 'Delete model'}
aria-label={
model.loaded
? 'Unload model before deleting'
: `Delete ${model.display_name}`
}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
) : showDownloading ? (
<Button size="sm" variant="outline" disabled>
<Button size="sm" variant="outline" disabled aria-label={`${model.display_name} downloading`}>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Downloading...
</Button>
) : (
<Button size="sm" onClick={onDownload} variant="outline">
<Button
size="sm"
onClick={onDownload}
variant="outline"
aria-label={`Download ${model.display_name}`}
>
<Download className="h-4 w-4 mr-2" />
Download
</Button>