From 9955e1dcb792d6d4760898f66779685b7073fdbb Mon Sep 17 00:00:00 2001 From: Richard Orme Date: Sat, 7 Mar 2026 12:36:02 -0800 Subject: [PATCH] a11y: address PR feedback and polish docs - HistoryTable: skip row key handler when focus is on Actions button (Enter/Space) - StoryList: expose selected story (aria-pressed, 'Selected' in label) - ProfileCard: skip card key handler when focus is on Export/Edit/Delete - VoicesTab: keep table semantics; edit button in first cell instead of role=button on row - PR-ACCESSIBILITY.md: 'Fine-tune' wording, 'focus on the text area' phrasing Made-with: Cursor --- app/src/components/History/HistoryTable.tsx | 2 +- app/src/components/StoriesTab/StoryList.tsx | 7 +++- .../components/VoiceProfiles/ProfileCard.tsx | 2 + app/src/components/VoicesTab/VoicesTab.tsx | 38 +++++++------------ docs/PR-ACCESSIBILITY.md | 4 +- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx index f6705c83..74f722b7 100644 --- a/app/src/components/History/HistoryTable.tsx +++ b/app/src/components/History/HistoryTable.tsx @@ -274,7 +274,7 @@ export function HistoryTable() { }} onKeyDown={(e) => { const target = e.target as HTMLElement; - if (target.closest('textarea')) return; + if (target.closest('textarea') || target.closest('button')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handlePlay(gen.id, gen.text, gen.profile_id); diff --git a/app/src/components/StoriesTab/StoryList.tsx b/app/src/components/StoriesTab/StoryList.tsx index ef36138d..a39a806d 100644 --- a/app/src/components/StoriesTab/StoryList.tsx +++ b/app/src/components/StoriesTab/StoryList.tsx @@ -200,7 +200,12 @@ export function StoryList() { 'h-24 p-4 border rounded-2xl transition-colors group flex items-center cursor-pointer', selectedStoryId === story.id && 'bg-muted border-primary', )} - aria-label={`Story ${story.name}, ${story.item_count} ${story.item_count === 1 ? 'item' : 'items'}, ${formatDate(story.updated_at)}. Press Enter to select.`} + aria-label={ + selectedStoryId === story.id + ? `Story ${story.name}, ${story.item_count} ${story.item_count === 1 ? 'item' : 'items'}, ${formatDate(story.updated_at)}. Selected. Press Enter to select.` + : `Story ${story.name}, ${story.item_count} ${story.item_count === 1 ? 'item' : 'items'}, ${formatDate(story.updated_at)}. Press Enter to select.` + } + aria-pressed={selectedStoryId === story.id} onClick={() => setSelectedStoryId(story.id)} onKeyDown={(e) => { if (e.target !== e.currentTarget) return; diff --git a/app/src/components/VoiceProfiles/ProfileCard.tsx b/app/src/components/VoiceProfiles/ProfileCard.tsx index 7768fdb8..2f13d957 100644 --- a/app/src/components/VoiceProfiles/ProfileCard.tsx +++ b/app/src/components/VoiceProfiles/ProfileCard.tsx @@ -62,6 +62,8 @@ export function ProfileCard({ profile }: ProfileCardProps) { }; const handleKeyDown = (e: React.KeyboardEvent) => { + const target = e.target as HTMLElement; + if (target.closest('button')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleSelect(); diff --git a/app/src/components/VoicesTab/VoicesTab.tsx b/app/src/components/VoicesTab/VoicesTab.tsx index 265d91e7..52f2f4cd 100644 --- a/app/src/components/VoicesTab/VoicesTab.tsx +++ b/app/src/components/VoicesTab/VoicesTab.tsx @@ -183,38 +183,28 @@ function VoiceRow({ const rowLabel = `${profile.name}, ${profile.language}, ${generationCount} generations, ${sampleCount} samples. Press Enter to edit.`; - const handleKeyDown = (e: React.KeyboardEvent) => { - const target = e.target as HTMLElement; - if (target.closest('button') || target.closest('[role="combobox"]') || target.closest('[role="listbox"]')) { - return; - } - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onEdit(); - } - }; - return ( - + -
+ e.stopPropagation()}>{profile.language} e.stopPropagation()}>{generationCount} diff --git a/docs/PR-ACCESSIBILITY.md b/docs/PR-ACCESSIBILITY.md index 61be4c4f..d9e71ac9 100644 --- a/docs/PR-ACCESSIBILITY.md +++ b/docs/PR-ACCESSIBILITY.md @@ -18,7 +18,7 @@ Improvements to support screen reader and keyboard users across the main app sur ### Generation UI (text box and voice choice) -- **Generate speech** (submit) and **Fine tune instructions** (sliders) – Icon buttons now have `aria-label` (and state for fine-tune, e.g. "Fine tune instructions, on"). +- **Generate speech** (submit) and **Fine-tune instructions** (sliders) – Icon buttons now have `aria-label` (and state for fine-tune, e.g. "Fine-tune instructions, on"). ### Voice selection (cards on Generate screen) @@ -28,7 +28,7 @@ Improvements to support screen reader and keyboard users across the main app sur ### History list (generated samples) - Each **sample row** is focusable with `role="button"` and an `aria-label` (e.g. "Sample from [profile], [duration], [date]. Press Enter to play."); **Enter/Space** plays or restarts. -- **Transcript textarea** has `aria-label` (e.g. "Transcript for sample from [profile], [duration]") so focus in the text area is announced in context. +- **Transcript textarea** has `aria-label` (e.g. "Transcript for sample from [profile], [duration]") so when you focus on the text area, the sample is announced in context. ### Voices tab (table)