Merge pull request #87 from madhura68/feat/a11y-audit-fixes

fix(a11y): static accessibility fixes (v1-readiness #4 — code-side)
This commit is contained in:
Janpeter Visser 2026-05-04 14:08:58 +02:00 committed by GitHub
commit 04181e54cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 42 additions and 25 deletions

View file

@ -5,7 +5,7 @@ import { QrLoginButton } from './qr-login-button'
export default function LoginPage() { export default function LoginPage() {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <main className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-sm space-y-6"> <div className="w-full max-w-sm space-y-6">
{/* Logo / titel */} {/* Logo / titel */}
@ -42,6 +42,6 @@ export default function LoginPage() {
</div> </div>
</div> </div>
</div> </main>
) )
} }

View file

@ -4,7 +4,7 @@ import { AuthForm } from '@/components/auth/auth-form'
export default function RegisterPage() { export default function RegisterPage() {
return ( return (
<div className="min-h-screen bg-background flex items-center justify-center p-4"> <main className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="w-full max-w-sm space-y-6"> <div className="w-full max-w-sm space-y-6">
{/* Logo / titel */} {/* Logo / titel */}
@ -26,6 +26,6 @@ export default function RegisterPage() {
</div> </div>
</div> </div>
</div> </main>
) )
} }

View file

@ -220,10 +220,11 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
{/* Title */} {/* Title */}
<div> <div>
<label className="text-sm font-medium mb-2 block"> <label htmlFor="task-title" className="text-sm font-medium mb-2 block">
Titel <span className="text-destructive">*</span> Titel <span className="text-destructive">*</span>
</label> </label>
<Input <Input
id="task-title"
{...form.register('title')} {...form.register('title')}
aria-invalid={!!form.formState.errors.title} aria-invalid={!!form.formState.errors.title}
autoFocus autoFocus
@ -240,13 +241,14 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
{/* Description */} {/* Description */}
<div> <div>
<label className="text-sm font-medium mb-2 block">Omschrijving</label> <label htmlFor="task-description" className="text-sm font-medium mb-2 block">Omschrijving</label>
<Controller <Controller
control={form.control} control={form.control}
name="description" name="description"
render={({ field }) => ( render={({ field }) => (
<> <>
<TextareaAutosize <TextareaAutosize
id="task-description"
{...field} {...field}
value={field.value ?? ''} value={field.value ?? ''}
aria-invalid={!!form.formState.errors.description} aria-invalid={!!form.formState.errors.description}
@ -275,13 +277,14 @@ export function TaskDialog({ task, storyId, productId, closePath, isDemo = false
{/* Implementation plan */} {/* Implementation plan */}
<div> <div>
<label className="text-sm font-medium mb-2 block">Implementatieplan</label> <label htmlFor="task-implementation-plan" className="text-sm font-medium mb-2 block">Implementatieplan</label>
<Controller <Controller
control={form.control} control={form.control}
name="implementation_plan" name="implementation_plan"
render={({ field }) => ( render={({ field }) => (
<> <>
<TextareaAutosize <TextareaAutosize
id="task-implementation-plan"
{...field} {...field}
value={field.value ?? ''} value={field.value ?? ''}
aria-invalid={!!form.formState.errors.implementation_plan} aria-invalid={!!form.formState.errors.implementation_plan}

View file

@ -192,8 +192,9 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
<div className="px-6 py-6 space-y-6"> <div className="px-6 py-6 space-y-6">
<div className="grid grid-cols-[6rem_1fr] gap-3"> <div className="grid grid-cols-[6rem_1fr] gap-3">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Code</label> <label htmlFor="story-code" className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Code</label>
<Input <Input
id="story-code"
name="code" name="code"
defaultValue={story?.code ?? ''} defaultValue={story?.code ?? ''}
placeholder={isEdit ? '' : 'auto'} placeholder={isEdit ? '' : 'auto'}
@ -205,10 +206,11 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
{fieldError('code') && <p className="text-xs text-error">{fieldError('code')}</p>} {fieldError('code') && <p className="text-xs text-error">{fieldError('code')}</p>}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <label htmlFor="story-title" className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Titel <span className="text-error">*</span> Titel <span className="text-error">*</span>
</label> </label>
<Input <Input
id="story-title"
ref={titleRef} ref={titleRef}
name="title" name="title"
defaultValue={story?.title ?? ''} defaultValue={story?.title ?? ''}
@ -223,15 +225,16 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">Prioriteit</label> <span id="story-priority-label" className="block text-xs font-medium text-muted-foreground uppercase tracking-wide">Prioriteit</span>
<PrioritySelect value={priority} onChange={(v) => { setPriority(v); setDirty(true) }} /> <PrioritySelect value={priority} onChange={(v) => { setPriority(v); setDirty(true) }} />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <label htmlFor="story-description" className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Omschrijving <span className="normal-case font-normal">(optioneel)</span> Omschrijving <span className="normal-case font-normal">(optioneel)</span>
</label> </label>
<Textarea <Textarea
id="story-description"
name="description" name="description"
rows={3} rows={3}
defaultValue={story?.description ?? ''} defaultValue={story?.description ?? ''}
@ -242,10 +245,11 @@ export function StoryDialog({ state, onClose, isDemo = false }: StoryDialogProps
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> <label htmlFor="story-acceptance" className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
Acceptatiecriteria <span className="normal-case font-normal">(optioneel)</span> Acceptatiecriteria <span className="normal-case font-normal">(optioneel)</span>
</label> </label>
<Textarea <Textarea
id="story-acceptance"
name="acceptance_criteria" name="acceptance_criteria"
rows={3} rows={3}
defaultValue={story?.acceptance_criteria ?? ''} defaultValue={story?.acceptance_criteria ?? ''}

View file

@ -63,11 +63,19 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) {
{job && ( {job && (
<span <span
className={cn( className={cn(
'text-[10px] px-1.5 py-0 rounded border flex items-center gap-1 shrink-0', 'text-[10px] px-1.5 py-0 rounded border flex items-center gap-1 shrink-0 cursor-pointer',
JOB_STATUS_COLORS[job.status], JOB_STATUS_COLORS[job.status],
)} )}
onClick={(e) => { e.stopPropagation(); onClick() }} onClick={(e) => { e.stopPropagation(); onClick() }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
e.stopPropagation()
onClick()
}
}}
role="button" role="button"
tabIndex={0}
aria-label={`Agent-status: ${JOB_STATUS_LABELS[job.status]}`} aria-label={`Agent-status: ${JOB_STATUS_LABELS[job.status]}`}
> >
{JOB_STATUS_ACTIVE.has(job.status) && <Loader2 className="animate-spin" size={8} />} {JOB_STATUS_ACTIVE.has(job.status) && <Loader2 className="animate-spin" size={8} />}

View file

@ -113,15 +113,16 @@ function PromotePbiDialog({
<form action={formAction} className="space-y-3"> <form action={formAction} className="space-y-3">
<input type="hidden" name="todoId" value={todo.id} /> <input type="hidden" name="todoId" value={todo.id} />
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Titel</label> <label htmlFor="promote-pbi-title" className="text-sm font-medium">Titel</label>
<Input name="title" defaultValue={todo.title} required /> <Input id="promote-pbi-title" name="title" defaultValue={todo.title} required />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Product</label> <label htmlFor="promote-pbi-product" className="text-sm font-medium">Product</label>
{products.length === 0 ? ( {products.length === 0 ? (
<p className="text-sm text-muted-foreground">Maak eerst een product aan.</p> <p className="text-sm text-muted-foreground">Maak eerst een product aan.</p>
) : ( ) : (
<select <select
id="promote-pbi-product"
name="productId" name="productId"
required required
defaultValue={todo.product_id ?? products[0]?.id} defaultValue={todo.product_id ?? products[0]?.id}
@ -132,8 +133,8 @@ function PromotePbiDialog({
)} )}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Prioriteit</label> <label htmlFor="promote-pbi-priority" className="text-sm font-medium">Prioriteit</label>
<select name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"> <select id="promote-pbi-priority" name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
<option value="1">Kritiek</option> <option value="1">Kritiek</option>
<option value="2">Hoog</option> <option value="2">Hoog</option>
<option value="3">Gemiddeld</option> <option value="3">Gemiddeld</option>
@ -184,15 +185,16 @@ function PromoteStoryDialog({
<input type="hidden" name="todoId" value={todo.id} /> <input type="hidden" name="todoId" value={todo.id} />
<input type="hidden" name="productId" value={selectedProductId} /> <input type="hidden" name="productId" value={selectedProductId} />
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Titel</label> <label htmlFor="promote-story-title" className="text-sm font-medium">Titel</label>
<Input name="title" defaultValue={todo.title} required /> <Input id="promote-story-title" name="title" defaultValue={todo.title} required />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Product</label> <label htmlFor="promote-story-product" className="text-sm font-medium">Product</label>
{products.length === 0 ? ( {products.length === 0 ? (
<p className="text-sm text-muted-foreground">Maak eerst een product aan.</p> <p className="text-sm text-muted-foreground">Maak eerst een product aan.</p>
) : ( ) : (
<select <select
id="promote-story-product"
value={selectedProductId} value={selectedProductId}
onChange={e => setSelectedProductId(e.target.value)} onChange={e => setSelectedProductId(e.target.value)}
className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background" className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"
@ -202,18 +204,18 @@ function PromoteStoryDialog({
)} )}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">PBI</label> <label htmlFor="promote-story-pbi" className="text-sm font-medium">PBI</label>
{!selectedProduct?.pbis.length ? ( {!selectedProduct?.pbis.length ? (
<p className="text-sm text-muted-foreground">Maak eerst een PBI aan in dit product.</p> <p className="text-sm text-muted-foreground">Maak eerst een PBI aan in dit product.</p>
) : ( ) : (
<select name="pbiId" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"> <select id="promote-story-pbi" name="pbiId" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
{selectedProduct.pbis.map(p => <option key={p.id} value={p.id}>{p.title}</option>)} {selectedProduct.pbis.map(p => <option key={p.id} value={p.id}>{p.title}</option>)}
</select> </select>
)} )}
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium">Prioriteit</label> <label htmlFor="promote-story-priority" className="text-sm font-medium">Prioriteit</label>
<select name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background"> <select id="promote-story-priority" name="priority" required className="w-full border border-border rounded-lg px-3 py-1.5 text-sm bg-input-background">
<option value="1">Kritiek</option> <option value="1">Kritiek</option>
<option value="2">Hoog</option> <option value="2">Hoog</option>
<option value="3">Gemiddeld</option> <option value="3">Gemiddeld</option>