mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
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
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 (
|
||||
<TableRow
|
||||
className="cursor-pointer"
|
||||
onClick={onEdit}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={rowLabel}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<TableRow className="cursor-pointer" onClick={onEdit}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full min-w-0 items-center gap-2 text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
|
||||
aria-label={rowLabel}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<div className="h-8 w-8 rounded-lg bg-muted flex items-center justify-center shrink-0">
|
||||
<Mic className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{profile.name}</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium truncate">{profile.name}</div>
|
||||
{profile.description && (
|
||||
<div className="text-sm text-muted-foreground">{profile.description}</div>
|
||||
<div className="text-sm text-muted-foreground truncate">{profile.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>{profile.language}</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>{generationCount}</TableCell>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user