# Accessibility Code Fixes - Detailed Implementation Guide ## Fix #1: File Upload Input aria-label **File**: `apps/web/components/listings/image-upload.tsx` **Line**: 118 ### Before ```tsx { if (e.target.files) addFiles(e.target.files); e.target.value = ''; }} /> ``` ### After ```tsx { if (e.target.files) addFiles(e.target.files); e.target.value = ''; }} /> ``` **Why**: Hidden inputs need aria-label so screen readers can announce their purpose when focused. --- ## Fix #2: Search Dialog Input aria-label **File**: `apps/web/app/[locale]/(public)/search/page.tsx` **Line**: 189 ### Before ```tsx setSaveName(e.target.value)} placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)" className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary" maxLength={100} onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()} /> ``` ### After ```tsx setSaveName(e.target.value)} placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)" aria-label="Tên bộ lọc tìm kiếm" className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary" maxLength={100} onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()} /> ``` **Why**: Text inputs need aria-label when no associated label element exists. Placeholder is not a substitute for aria-label. --- ## Fix #3: Admin Moderation - Select All Checkbox **File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx` **Line**: 222 ### Before ```tsx 0} onChange={toggleSelectAll} className="rounded border-input" /> ``` ### After ```tsx 0} onChange={toggleSelectAll} className="rounded border-input" /> ``` **Why**: Checkbox in table headers need aria-label to distinguish them from row checkboxes. --- ## Fix #4: Admin Moderation - Row Checkboxes **File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx` **Line**: 242 ### Before ```tsx toggleSelect(item.listingId)} className="rounded border-input" /> ``` ### After (Option 1 - Simple) ```tsx toggleSelect(item.listingId)} className="rounded border-input" /> ``` ### After (Option 2 - Better with title) ```tsx toggleSelect(item.listingId)} className="rounded border-input" /> ``` **Why**: Each checkbox needs unique aria-label that includes context about what listing it represents. --- ## Fix #5: Test Mock Image Component **File**: `apps/web/app/[locale]/(public)/search/__tests__/search.spec.tsx` **Line**: 46 ### Before ```tsx vi.mock('next/image', () => ({ default: (props: Record) => , })); ``` ### After (Option 1 - Simple) ```tsx vi.mock('next/image', () => ({ default: (props: Record) => {props.alt, })); ``` ### After (Option 2 - With Warning) ```tsx vi.mock('next/image', () => ({ default: (props: Record) => { if (!props.alt) { console.warn('Image mock: Missing alt attribute', props); } return {props.alt; }, })); ``` **Why**: Mock should enforce alt attribute to catch missing alts in tests before production. --- ## Fix #6 (Enhancement): Image Upload Drag-Drop Accessibility **File**: `apps/web/components/listings/image-upload.tsx` **Lines**: 86-128 ### Current Code ```tsx
inputRef.current?.click()} className={cn( 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors', isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-primary/50', )} > ...

Kéo thả ảnh vào đây hoặc nhấp để chọn

JPG, PNG, WebP - Tối đa {maxFiles} ảnh, mỗi ảnh 10MB

{ if (e.target.files) addFiles(e.target.files); e.target.value = ''; }} />
``` ### Enhanced Code ```tsx
inputRef.current?.click()} role="button" tabIndex={0} aria-label="Khu vực kéo thả hoặc nhấp để tải ảnh lên" onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); inputRef.current?.click(); } }} className={cn( 'flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-8 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2', isDragging ? 'border-primary bg-primary/5' : 'border-muted-foreground/25 hover:border-primary/50', )} > ...

Kéo thả ảnh vào đây hoặc nhấp để chọn

JPG, PNG, WebP - Tối đa {maxFiles} ảnh, mỗi ảnh 10MB

{ if (e.target.files) addFiles(e.target.files); e.target.value = ''; }} />
``` **Changes**: - `role="button"` - Identifies div as an interactive button - `tabIndex={0}` - Makes div keyboard accessible - `aria-label` - Describes the purpose to screen readers - `onKeyDown` handler - Allows Enter/Space to activate - `focus-visible` styles - Shows focus indicator for keyboard navigation **Why**: Makes drag-drop area fully keyboard accessible for users who can't use a mouse. --- ## Testing After Implementation ### 1. Screen Reader Testing ```bash # Use VoiceOver (Mac), NVDA (Windows), or JAWS # Navigate to each fixed element and verify: # - Input is announced with its aria-label # - Checkbox is announced with its aria-label # - Purpose is clear from screen reader announcement ``` ### 2. Keyboard Navigation Testing ```bash # Tab through the page # Verify: # - All interactive elements are reachable via Tab # - Focus is visible on all elements # - Enter/Space activates buttons and checkboxes # - Image upload drag area is focused and can be activated with keyboard ``` ### 3. Automated Testing ```bash # Run axe npm run test:a11y # Or use Lighthouse npx lighthouse https://localhost:3000 --view # ESLint JSX Accessibility Plugin should catch these issues: npm run lint ``` ### 4. Visual Testing ```bash # Verify with browser dev tools: # - Inspect each input to confirm aria-label attribute exists # - Check for proper focus styles # - Verify focus ring colors meet contrast requirements ``` --- ## Summary of Changes | Issue | File | Line | Type | Severity | |-------|------|------|------|----------| | File input missing aria-label | image-upload.tsx | 118 | aria-label | HIGH | | Search input missing aria-label | search/page.tsx | 189 | aria-label | HIGH | | Header checkbox missing aria-label | moderation/page.tsx | 222 | aria-label | HIGH | | Row checkboxes missing aria-label | moderation/page.tsx | 242 | aria-label | HIGH | | Mock Image missing alt | search.spec.tsx | 46 | alt attribute | MEDIUM | | Drag-drop area not keyboard accessible | image-upload.tsx | 86-128 | enhancement | MEDIUM | --- ## Estimated Implementation Time - Fix #1: 2 minutes - Fix #2: 2 minutes - Fix #3: 2 minutes - Fix #4: 3 minutes (need to find item.title in context) - Fix #5: 2 minutes - Fix #6: 10 minutes - Testing: 15-20 minutes **Total: ~35-45 minutes**