Root directory had accumulated audit/exploration markdown files cluttering the project root. Moved all audit-related files to docs/audits/ with a README.md index, and updated cross-references in K6_LOAD_TESTING_GUIDE.md and README_FRONTEND_DOCS.md. Co-Authored-By: Paperclip <noreply@paperclip.ing>
289 lines
8.4 KiB
Markdown
289 lines
8.4 KiB
Markdown
# GoodGo Frontend Accessibility Issues - Code Fixes Required
|
|
|
|
**Date**: 2026-04-10
|
|
**Scope**: apps/web (GoodGo Frontend)
|
|
**Status**: ACTIONABLE ITEMS - Ready for Implementation
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
Found **4 specific accessibility issues** that require code fixes across the GoodGo frontend. Below are the exact file paths, line numbers, problematic code snippets, and required fixes.
|
|
|
|
---
|
|
|
|
## ISSUE 1: Form Inputs Missing aria-label or Associated Labels
|
|
|
|
### 1.1 File Upload Input Without aria-label
|
|
**File**: `apps/web/components/listings/image-upload.tsx`
|
|
**Line**: 118
|
|
**Problem**: Hidden file input has no aria-label or associated label element
|
|
|
|
**Current Code**:
|
|
```tsx
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
if (e.target.files) addFiles(e.target.files);
|
|
e.target.value = '';
|
|
}}
|
|
/>
|
|
```
|
|
|
|
**Fix Required**: Add `aria-label`
|
|
```tsx
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
aria-label="Chọn ảnh để tải lên"
|
|
onChange={(e) => {
|
|
if (e.target.files) addFiles(e.target.files);
|
|
e.target.value = '';
|
|
}}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.2 Search Save Dialog - Text Input Without aria-label
|
|
**File**: `apps/web/app/[locale]/(public)/search/page.tsx`
|
|
**Line**: 189
|
|
**Problem**: Text input for saving search name has no associated label or aria-label
|
|
|
|
**Current Code**:
|
|
```tsx
|
|
<input
|
|
type="text"
|
|
value={saveName}
|
|
onChange={(e) => 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()}
|
|
/>
|
|
```
|
|
|
|
**Fix Required**: Add `aria-label`
|
|
```tsx
|
|
<input
|
|
type="text"
|
|
value={saveName}
|
|
onChange={(e) => 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()}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.3 Admin Moderation - Select All Checkbox Without aria-label
|
|
**File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
|
|
**Line**: 222
|
|
**Problem**: Table header checkbox for "select all" has no aria-label
|
|
|
|
**Current Code**:
|
|
```tsx
|
|
<TableHead className="w-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.size === result.data.length && result.data.length > 0}
|
|
onChange={toggleSelectAll}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableHead>
|
|
```
|
|
|
|
**Fix Required**: Add `aria-label`
|
|
```tsx
|
|
<TableHead className="w-10">
|
|
<input
|
|
type="checkbox"
|
|
aria-label="Chọn tất cả tin đăng"
|
|
checked={selected.size === result.data.length && result.data.length > 0}
|
|
onChange={toggleSelectAll}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableHead>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.4 Admin Moderation - Row Checkboxes Without aria-label
|
|
**File**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
|
|
**Line**: 242
|
|
**Problem**: Individual row checkboxes in table have no aria-label
|
|
|
|
**Current Code**:
|
|
```tsx
|
|
<TableCell>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected.has(item.listingId)}
|
|
onChange={() => toggleSelect(item.listingId)}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableCell>
|
|
```
|
|
|
|
**Fix Required**: Add `aria-label` with dynamic content
|
|
```tsx
|
|
<TableCell>
|
|
<input
|
|
type="checkbox"
|
|
aria-label={`Chọn tin đăng: ${item.title || item.listingId}`}
|
|
checked={selected.has(item.listingId)}
|
|
onChange={() => toggleSelect(item.listingId)}
|
|
className="rounded border-input"
|
|
/>
|
|
</TableCell>
|
|
```
|
|
|
|
---
|
|
|
|
## ISSUE 2: Mock Image Component Missing alt Attribute
|
|
|
|
### 2.1 Test Mock Image Component
|
|
**File**: `apps/web/app/[locale]/(public)/search/__tests__/search.spec.tsx`
|
|
**Line**: 46
|
|
**Problem**: Mock Image component spreads all props including missing alt attribute
|
|
|
|
**Current Code**:
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => <img {...props} />,
|
|
```
|
|
|
|
**Fix Required**: Ensure alt is always included in mock or add default
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => <img {...props} alt={props.alt || ''} />,
|
|
```
|
|
|
|
OR better approach - require alt in mock setup:
|
|
```tsx
|
|
default: (props: Record<string, unknown>) => {
|
|
if (!props.alt) {
|
|
console.warn('Missing alt attribute in Image mock:', props);
|
|
}
|
|
return <img {...props} alt={props.alt || 'image'} />;
|
|
},
|
|
```
|
|
|
|
---
|
|
|
|
## ISSUE 3: Hidden File Input Needs Better Accessibility
|
|
|
|
### 3.1 Image Upload Drag-Drop Area Needs Better Labeling
|
|
**File**: `apps/web/components/listings/image-upload.tsx`
|
|
**Lines**: 86-128
|
|
**Problem**: The clickable div that triggers file input has descriptive text but no label element linking to the hidden input
|
|
|
|
**Current Implementation**:
|
|
```tsx
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => inputRef.current?.click()}
|
|
className={cn(...)}
|
|
>
|
|
<svg>...</svg>
|
|
<p className="text-sm font-medium">Kéo thả ảnh vào đây hoặc nhấp để chọn</p>
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
JPG, PNG, WebP - Tối đa {maxFiles} ảnh, mỗi ảnh 10MB
|
|
</p>
|
|
<input
|
|
ref={inputRef}
|
|
type="file"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => {...}}
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
**Recommended Enhancement**: Add proper label or role
|
|
```tsx
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
onClick={() => 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 === ' ') {
|
|
inputRef.current?.click();
|
|
}
|
|
}}
|
|
className={cn(...)}
|
|
>
|
|
{/* ... rest of content ... */}
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
## ISSUE 4: Verification of Properly Implemented Accessibility
|
|
|
|
### ✅ CORRECT - Image Components with alt attributes
|
|
The following files already have proper alt attributes and require NO changes:
|
|
- `apps/web/components/listings/image-gallery.tsx` - All Image components have alt (lines 34, 77)
|
|
- `apps/web/components/listings/image-upload.tsx` - All img tags have alt (line 135-138)
|
|
- `apps/web/components/search/property-card.tsx` - Image has alt (line 44)
|
|
- `apps/web/app/[locale]/(dashboard)/listings/page.tsx` - All Images have alt (lines 192, 272)
|
|
- `apps/web/app/[locale]/(dashboard)/dashboard/page.tsx` - Image has alt (line 252)
|
|
- `apps/web/app/[locale]/(admin)/admin/kyc/page.tsx` - All Images have alt (lines 102, 116, 130)
|
|
|
|
### ✅ CORRECT - Icon-only Buttons with aria-label
|
|
The following files already have proper aria-labels and require NO changes:
|
|
- `apps/web/components/listings/image-gallery.tsx` - Navigation buttons have aria-labels (lines 47, 54)
|
|
- `apps/web/app/[locale]/(public)/layout.tsx` - Mobile menu button has aria-label (line 91)
|
|
|
|
### ✅ CORRECT - Dialogs with Semantic Titles
|
|
The following dialogs already have proper DialogTitle elements and require NO changes:
|
|
- `apps/web/app/[locale]/(dashboard)/dashboard/subscription/page.tsx` - DialogTitle present (line 327-329)
|
|
- `apps/web/app/[locale]/(admin)/admin/kyc/page.tsx` - Both dialogs have DialogTitle (approval and rejection dialogs)
|
|
|
|
### ✅ CORRECT - Checkbox with Associated Label
|
|
- `apps/web/app/[locale]/(public)/search/page.tsx` (line 199) - Checkbox has associated `<label>` element
|
|
|
|
---
|
|
|
|
## Implementation Priority
|
|
|
|
**Priority 1 (High Impact)**:
|
|
1. Add aria-label to file input (image-upload.tsx:118)
|
|
2. Add aria-label to search name input (search/page.tsx:189)
|
|
|
|
**Priority 2 (High Impact)**:
|
|
3. Add aria-label to table checkboxes (moderation/page.tsx:222, 242)
|
|
4. Fix mock Image component to require alt (search.spec.tsx:46)
|
|
|
|
**Priority 3 (Enhancement)**:
|
|
5. Improve image upload drag-drop area accessibility with role and keyboard support (image-upload.tsx:86-128)
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
After implementing fixes, verify:
|
|
- [ ] Screen readers announce all form inputs correctly
|
|
- [ ] File input has meaningful aria-label when focused
|
|
- [ ] Search dialog inputs are accessible via keyboard
|
|
- [ ] Table checkboxes have descriptive labels for each row
|
|
- [ ] No console warnings about missing alt attributes in tests
|
|
- [ ] Keyboard navigation works for all interactive elements
|
|
- [ ] WCAG 2.1 Level AA compliance verified with automated tools
|
|
|