# Sửa Lỗi Trợ Năng - Hướng Dẫn Triển Khai Chi Tiết
## Sửa #1: aria-label cho Input Tải Tệp Lên
**Tệp**: `apps/web/components/listings/image-upload.tsx`
**Dòng**: 118
### Trước
```tsx
{
if (e.target.files) addFiles(e.target.files);
e.target.value = '';
}}
/>
```
### Sau
```tsx
{
if (e.target.files) addFiles(e.target.files);
e.target.value = '';
}}
/>
```
**Lý do**: Các input ẩn cần aria-label để trình đọc màn hình có thể thông báo mục đích của chúng khi được lấy tiêu điểm.
---
## Sửa #2: aria-label cho Input Hộp Thoại Tìm Kiếm
**Tệp**: `apps/web/app/[locale]/(public)/search/page.tsx`
**Dòng**: 189
### Trước
```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()}
/>
```
### Sau
```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()}
/>
```
**Lý do**: Các input văn bản cần aria-label khi không có phần tử nhãn liên kết. Placeholder không thể thay thế cho aria-label.
---
## Sửa #3: Quản Trị Kiểm Duyệt - Checkbox Chọn Tất Cả
**Tệp**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
**Dòng**: 222
### Trước
```tsx
0}
onChange={toggleSelectAll}
className="rounded border-input"
/>
```
### Sau
```tsx
0}
onChange={toggleSelectAll}
className="rounded border-input"
/>
```
**Lý do**: Checkbox trong tiêu đề bảng cần aria-label để phân biệt với các checkbox trên từng hàng.
---
## Sửa #4: Quản Trị Kiểm Duyệt - Checkbox Từng Hàng
**Tệp**: `apps/web/app/[locale]/(admin)/admin/moderation/page.tsx`
**Dòng**: 242
### Trước
```tsx
toggleSelect(item.listingId)}
className="rounded border-input"
/>
```
### Sau (Phương án 1 - Đơn giản)
```tsx
toggleSelect(item.listingId)}
className="rounded border-input"
/>
```
### Sau (Phương án 2 - Tốt hơn với tiêu đề)
```tsx
toggleSelect(item.listingId)}
className="rounded border-input"
/>
```
**Lý do**: Mỗi checkbox cần aria-label duy nhất bao gồm ngữ cảnh về tin đăng mà nó đại diện.
---
## Sửa #5: Component Ảnh Giả Lập trong Kiểm Thử
**Tệp**: `apps/web/app/[locale]/(public)/search/__tests__/search.spec.tsx`
**Dòng**: 46
### Trước
```tsx
vi.mock('next/image', () => ({
default: (props: Record) => ,
}));
```
### Sau (Phương án 1 - Đơn giản)
```tsx
vi.mock('next/image', () => ({
default: (props: Record) => ,
}));
```
### Sau (Phương án 2 - Có Cảnh Báo)
```tsx
vi.mock('next/image', () => ({
default: (props: Record) => {
if (!props.alt) {
console.warn('Image mock: Missing alt attribute', props);
}
return ;
},
}));
```
**Lý do**: Bản giả lập nên bắt buộc thuộc tính alt để phát hiện các ảnh thiếu alt trong kiểm thử trước khi đưa lên môi trường production.
---
## Sửa #6 (Cải Tiến): Trợ Năng Kéo-Thả Tải Ảnh Lên
**Tệp**: `apps/web/components/listings/image-upload.tsx`
**Dòng**: 86-128
### Mã Hiện Tại
```tsx
{
if (e.target.files) addFiles(e.target.files);
e.target.value = '';
}}
/>
```
**Các thay đổi**:
- `role="button"` - Xác định div là một nút tương tác
- `tabIndex={0}` - Cho phép div nhận tiêu điểm từ bàn phím
- `aria-label` - Mô tả mục đích cho trình đọc màn hình
- Trình xử lý `onKeyDown` - Cho phép kích hoạt bằng Enter/Space
- Kiểu `focus-visible` - Hiển thị chỉ báo tiêu điểm khi điều hướng bằng bàn phím
**Lý do**: Làm cho vùng kéo-thả có thể truy cập hoàn toàn bằng bàn phím cho những người dùng không thể sử dụng chuột.
---
## Kiểm Tra Sau Khi Triển Khai
### 1. Kiểm Tra Trình Đọc Màn Hình
```bash
# Sử dụng VoiceOver (Mac), NVDA (Windows), hoặc JAWS
# Điều hướng đến từng phần tử đã sửa và xác nhận:
# - Input được thông báo với aria-label của nó
# - Checkbox được thông báo với aria-label của nó
# - Mục đích rõ ràng qua thông báo của trình đọc màn hình
```
### 2. Kiểm Tra Điều Hướng Bằng Bàn Phím
```bash
# Tab qua trang
# Xác nhận:
# - Tất cả các phần tử tương tác có thể truy cập bằng Tab
# - Tiêu điểm hiển thị rõ trên tất cả các phần tử
# - Enter/Space kích hoạt các nút và checkbox
# - Vùng kéo-thả tải ảnh có thể nhận tiêu điểm và kích hoạt bằng bàn phím
```
### 3. Kiểm Tra Tự Động
```bash
# Chạy axe
npm run test:a11y
# Hoặc dùng Lighthouse
npx lighthouse https://localhost:3000 --view
# Plugin ESLint JSX Accessibility nên phát hiện các vấn đề này:
npm run lint
```
### 4. Kiểm Tra Trực Quan
```bash
# Xác nhận bằng công cụ dành cho nhà phát triển trong trình duyệt:
# - Kiểm tra từng input để xác nhận thuộc tính aria-label tồn tại
# - Kiểm tra kiểu tiêu điểm phù hợp
# - Xác nhận màu vòng tiêu điểm đáp ứng yêu cầu độ tương phản
```
---
## Tóm Tắt Các Thay Đổi
| Vấn đề | Tệp | Dòng | Loại | Mức độ nghiêm trọng |
|--------|-----|------|------|---------------------|
| Input tệp thiếu aria-label | image-upload.tsx | 118 | aria-label | CAO |
| Input tìm kiếm thiếu aria-label | search/page.tsx | 189 | aria-label | CAO |
| Checkbox tiêu đề thiếu aria-label | moderation/page.tsx | 222 | aria-label | CAO |
| Checkbox hàng thiếu aria-label | moderation/page.tsx | 242 | aria-label | CAO |
| Ảnh giả lập thiếu alt | search.spec.tsx | 46 | thuộc tính alt | TRUNG BÌNH |
| Vùng kéo-thả không thể truy cập bằng bàn phím | image-upload.tsx | 86-128 | cải tiến | TRUNG BÌNH |
---
## Thời Gian Triển Khai Ước Tính
- Sửa #1: 2 phút
- Sửa #2: 2 phút
- Sửa #3: 2 phút
- Sửa #4: 3 phút (cần tìm item.title trong ngữ cảnh)
- Sửa #5: 2 phút
- Sửa #6: 10 phút
- Kiểm tra: 15-20 phút
**Tổng cộng: ~35-45 phút**