# Báo Cáo Kiểm Tra Toàn Diện Khả Năng Tiếp Cận Frontend GoodGo Platform
**Ngày:** 10 tháng 4, 2026
**Đối tượng kiểm tra:** apps/web (Next.js 15)
**Tổng số tệp đã phân tích:** 90+ tệp TSX/JSX
**Thuộc tính ARIA tìm thấy:** 75 trường hợp trên 14 tệp
---
## Tóm Tắt Điều Hành
Giao diện frontend của GoodGo Platform thể hiện **các thực hành khả năng tiếp cận nền tảng tốt** với liên kết bỏ qua nội dung, các vai trò HTML ngữ nghĩa phù hợp, nhãn ARIA trên các phần tử tương tác và xác thực biểu mẫu. Tuy nhiên, có một số lĩnh vực cần cải thiện:
**Phát hiện chính:**
- ✅ Đã triển khai liên kết bỏ qua nội dung
- ✅ 75 thuộc tính ARIA trên toàn codebase
- ✅ Thành phần Dialog với quản lý tiêu điểm
- ✅ Xử lý lỗi với các thuộc tính ARIA phù hợp
- ⚠️ Một số nơi còn thiếu aria-labels cho các nút chỉ có biểu tượng
- ⚠️ Các nút ảnh thu nhỏ trong thư viện hình ảnh thiếu tên có thể tiếp cận
- ⚠️ Thành phần Dialog thiếu các tính năng khả năng tiếp cận (role, focus trap)
- ⚠️ Chưa có tài liệu về định nghĩa độ tương phản màu sắc
- ❌ Một số ô nhập biểu mẫu thiếu liên kết nhãn phù hợp
---
## 1. PHÂN TÍCH SỬ DỤNG ARIA HIỆN TẠI
### Tóm Tắt
- **Tổng số thuộc tính ARIA:** 75 trường hợp
- **Các tệp sử dụng ARIA:** 14 tệp
- **Phổ biến nhất:** aria-label (41 trường hợp), aria-hidden (17 trường hợp)
### Phân Tích ARIA Chi Tiết Theo Tệp
#### Layout Công Khai (`apps/web/app/[locale]/(public)/layout.tsx`)
**Dòng 49:** `aria-label={t('nav.mainNav')}`
- Vị trí: Phần tử điều hướng trên máy tính để bàn
- Loại: Điểm mốc điều hướng ngữ nghĩa
**Dòng 91:** `aria-label={mobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')}`
- Vị trí: Nút hamburger trên di động
- Loại: Nhãn được cập nhật động dựa trên trạng thái
- Trạng thái: ✅ Được triển khai đúng cách
#### Layout Gốc (`apps/web/app/[locale]/layout.tsx`)
**Dòng 105-109:** Liên kết bỏ qua nội dung
```tsx
{t('skipToContent')}
```
- Trạng thái: ✅ Được triển khai đúng cách với khả năng hiển thị tiêu điểm
- Ẩn theo mặc định với -translate-y-16, hiển thị khi được tiêu điểm
- Liên kết đến `id="main-content"` trên phần tử main
#### Layout Dashboard (`apps/web/app/[locale]/(dashboard)/layout.tsx`)
**Dòng 47:** `aria-label={t('nav.dashboardNav')}`
- Vị trí: Điều hướng thanh bên trên di động
- Loại: Điểm mốc điều hướng
**Dòng 58:** `aria-label={t('nav.closeMenu')}`
- Vị trí: Nút đóng thanh bên trên di động
- Loại: Nút chỉ có biểu tượng với nhãn
**Dòng 79:** `aria-hidden="true"`
- Vị trí: Biểu tượng Emoji trong các mục điều hướng
- Loại: Ẩn nội dung trang trí
**Dòng 95:** `aria-hidden="true"`
- Vị trí: Biểu tượng LogOut
- Loại: Ẩn biểu tượng trang trí
**Dòng 108:** `aria-label={t('nav.openMenu')}`
- Vị trí: Nút hamburger trên di động
- Loại: Nút chỉ có biểu tượng
**Dòng 120:** `aria-label={t('nav.dashboardNav')}`
- Vị trí: Điều hướng trên máy tính để bàn
- Loại: Điểm mốc điều hướng
**Dòng 125:** `aria-label={item.label}`
- Vị trí: Các mục điều hướng
- Loại: Nhãn dư thừa (văn bản đã hiển thị)
- Trạng thái: ⚠️ Không cần thiết khi văn bản đã hiển thị
**Dòng 133:** `aria-hidden="true"`
- Vị trí: Biểu tượng trong các mục điều hướng
- Loại: Nội dung trang trí
**Dòng 150:** `aria-label={theme === 'light' ? t('dashboard.darkMode') : t('dashboard.lightMode')}`
- Vị trí: Nút chuyển đổi chủ đề
- Loại: aria-label động ✅ Tốt
**Dòng 154, 158:** `aria-hidden="true"` trên SVG
- Vị trí: Biểu tượng chuyển đổi chủ đề
- Loại: Ẩn nội dung trang trí
#### Layout Quản Trị (`apps/web/app/[locale]/(admin)/layout.tsx`)
**Dòng 60:** `aria-hidden="true"`
- Vị trí: Nền phủ di động
**Dòng 67:** `aria-label={t('nav.adminNav')}`
- Vị trí: Điều hướng thanh bên
**Dòng 81:** `aria-label={t('adminNav.closeMenu')}`
- Vị trí: Nút đóng
**Dòng 89:** `aria-label={t('nav.adminNav')}`
- Vị trí: Vùng chứa điều hướng (trùng lặp)
**Dòng 108:** `aria-hidden="true"`
- Vị trí: Biểu tượng điều hướng
**Dòng 126:** `aria-hidden="true"`
- Vị trí: Biểu tượng LogOut
**Dòng 135:** `aria-label={t('adminNav.openMenu')}`
- Vị trí: Nút hamburger trên di động
#### Bộ Chuyển Đổi Ngôn Ngữ (`apps/web/components/ui/language-switcher.tsx`)
**Dòng 29:** `aria-label={`${t('label')}: ${t(locale)} → ${t(nextLocale)}`}`
- Vị trí: Nút chuyển đổi ngôn ngữ
- Trạng thái: ✅ Nhãn mô tả cho biết hành động
**Dòng 31:** `aria-hidden="true"`
- Vị trí: Hiển thị Emoji
- Loại: Nội dung trang trí
#### Thành Phần Tìm Kiếm/Bộ Lọc (`apps/web/components/search/filter-bar.tsx`)
**Dòng 79:** `role="search" aria-label={t('filters')}`
- Vị trí: Phần bộ lọc
- Trạng thái: ✅ Vai trò ngữ nghĩa phù hợp với nhãn
**Dòng 87, 101, 115, 129:** `aria-label` trên các ô nhập chọn
- Vị trí: Danh sách thả xuống bộ lọc
- Mẫu: `aria-label={t('allTransactions')}`, `aria-label={t('allPropertyTypes')}`, v.v.
- Trạng thái: ✅ Điều khiển biểu mẫu có thể tiếp cận
**Dòng 150, 158, 167, 182, 192:** `aria-label` trên các ô nhập phạm vi
- Vị trí: Bộ lọc diện tích và số phòng ngủ
- Trạng thái: ✅ Nhãn rõ ràng cho các phạm vi nhập
#### Thư Viện Hình Ảnh (`apps/web/components/listings/image-gallery.tsx`)
**Dòng 47:** `aria-label="Ảnh trước"` (Hình ảnh trước)
- Vị trí: Nút trước
- Trạng thái: ✅ Nút chỉ có biểu tượng với aria-label
**Dòng 54:** `aria-label="Ảnh tiếp"` (Hình ảnh tiếp theo)
- Vị trí: Nút tiếp theo
- Trạng thái: ✅ Nút chỉ có biểu tượng với aria-label
#### Thẻ Bất Động Sản (`apps/web/components/search/property-card.tsx`)
**Dòng 36:** `aria-label={`${listing.property.title} — ${transactionLabel} ${propertyTypeLabel}, ${formatPrice(listing.priceVND)} VNĐ`}`
- Vị trí: Phần tử article bao quanh thẻ bất động sản
- Trạng thái: ✅ Tên có thể tiếp cận mô tả cho thẻ
**Dòng 50:** `aria-hidden="true"`
- Vị trí: Chỗ giữ chỗ "Không có hình ảnh"
- Loại: Nội dung trang trí
**Dòng 64:** `aria-label={`${listing.property.media.length} ảnh`}`
- Vị trí: Huy hiệu số lượng hình ảnh
- Trạng thái: ⚠️ Huy hiệu mang tính trang trí, không cần aria-label
**Dòng 81:** `aria-label="Thông tin bất động sản"`
- Vị trí: Danh sách chi tiết bất động sản
- Trạng thái: ✅ Danh sách với nhãn ngữ nghĩa
#### Các Trang Xác Thực
**Trang Đăng Nhập (`apps/web/app/[locale]/(auth)/login/page.tsx`)**
- Dòng 76: `aria-describedby={errors.phone ? 'phone-error' : undefined}`
- Dòng 77: `aria-invalid={!!errors.phone}`
- Dòng 92: `aria-label={showPassword ? t('hidePassword') : t('showPassword')}`
- Dòng 102: `aria-describedby={errors.password ? 'password-error' : undefined}`
- Dòng 103: `aria-invalid={!!errors.password}`
- Dòng 112: `aria-hidden="true"` trên vòng xoay tải
- Trạng thái: ✅ Khả năng tiếp cận biểu mẫu toàn diện
**Trang Đăng Ký (`apps/web/app/[locale]/(auth)/register/page.tsx`)**
- Mẫu tương tự trang đăng nhập với aria-describedby và aria-invalid
- Dòng 70, 71, 86, 87, 102, 103, 118, 128, 129, 144, 145
- Trạng thái: ✅ Được triển khai đúng cách
#### Trang Đích/Trang Chủ (`apps/web/app/[locale]/(public)/page.tsx`)
**Dòng 85:** `role="search" aria-label={t('common.search')}`
- Vị trí: Biểu mẫu tìm kiếm chính
- Trạng thái: ✅ Vai trò ngữ nghĩa phù hợp
**Dòng 92:** `aria-label={t('landing.searchPlaceholder')}`
- Vị trí: Ô nhập tìm kiếm
- Trạng thái: ⚠️ Dư thừa với placeholder
**Dòng 99:** `aria-label={t('landing.transactionTypeLabel')}`
- Vị trí: Lựa chọn loại giao dịch
- Trạng thái: ✅ Tốt cho khả năng tiếp cận biểu mẫu
**Dòng 114:** `aria-hidden="true"`
- Vị trí: SVG trang trí
**Dòng 147:** `aria-labelledby="featured-heading"`
- Vị trí: Phần danh sách nổi bật
- Trạng thái: ✅ Phần được gắn nhãn đúng cách
**Dòng 162:** `role="status" aria-label={t('common.loading')}`
- Vị trí: Chỉ báo tải
- Trạng thái: ✅ Vai trò phù hợp cho thông báo trạng thái
**Dòng 163:** `aria-hidden="true"`
- Vị trí: Hoạt ảnh vòng xoay
**Dòng 188:** `aria-labelledby="districts-heading"`
- Vị trí: Phần quận huyện
- Trạng thái: ✅ Nhãn phần
**Dòng 204:** `aria-hidden="true"`
- Vị trí: Trang trí Emoji
**Dòng 219:** `aria-labelledby="stats-heading"`
- Vị trí: Phần thống kê
- Trạng thái: ✅ Nhãn phần
**Dòng 234:** `aria-hidden="true"`
- Vị trí: Biểu tượng Emoji trong thống kê
#### Trang Lỗi và Không Tìm Thấy
**error.tsx:**
- Dòng 51: `aria-hidden="true"` trên emoji trang trí
- Dòng 85: `aria-hidden="true"` trên vòng xoay SVG
**not-found.tsx:**
- Dòng 10: `aria-hidden="true"` trên số 404
#### Kiểm Thử Thành Phần UI (`components/ui/__tests__/select.spec.tsx`)
- Dòng 9, 22, 34: `aria-label` trên các thành phần select
- Trạng thái: ✅ Kiểm thử xác minh khả năng tiếp cận
---
## 2. PHÂN TÍCH CÁC NÚT CHỈ CÓ BIỂU TƯỢNG
### Các Nút Cần aria-label
#### ✅ Các Nút Biểu Tượng Được Gắn Nhãn Đúng Cách
1. **Nút Chuyển Đổi Menu Di Động** (`apps/web/app/[locale]/(public)/layout.tsx:90-96`)
```tsx
```
- Trạng thái: ✅ aria-label động dựa trên trạng thái
2. **Nút Chuyển Đổi Chủ Đề** (`apps/web/app/[locale]/(dashboard)/layout.tsx:146-160`)
```tsx
```
- Trạng thái: ✅ Nút phù hợp với biểu tượng và aria-label
3. **Bộ Chuyển Đổi Ngôn Ngữ** (`apps/web/components/ui/language-switcher.tsx:25-34`)
```tsx
```
- Trạng thái: ✅ Sử dụng cả aria-label và sr-only để tăng tính dự phòng
4. **Điều Hướng Thư Viện Hình Ảnh** (`apps/web/components/listings/image-gallery.tsx:44-57`)
```tsx
```
- Trạng thái: ✅ Các nút Trước/Tiếp theo có nhãn rõ ràng
#### ⚠️ Các Nút Chỉ Có Biểu Tượng Cần Chú Ý
1. **Các Nút Ảnh Thu Nhỏ** (`apps/web/components/listings/image-gallery.tsx:69-84`)
```tsx
```
- **Vấn đề:** Không có aria-label trên các nút ảnh thu nhỏ
- **Tác động:** Người dùng trình đọc màn hình không thể xác định ảnh thu nhỏ nào họ đang chọn
- **Sửa:** Thêm `aria-label={`Select image ${index + 1}`}`
2. **Nút Đóng Di Động Admin/Dashboard** (`apps/web/app/[locale]/(admin)/layout.tsx:80-86`)
```tsx
```
- Trạng thái: ✅ Đã có aria-label (đã xác minh)
3. **Nút Đóng Di Động Dashboard** (`apps/web/app/[locale]/(dashboard)/layout.tsx:57-63`)
```tsx
```
- Trạng thái: ✅ Đã có aria-label (đã xác minh)
4. **Nút Menu Di Động Admin** (`apps/web/app/[locale]/(admin)/layout.tsx:135`)
```tsx
```
- Trạng thái: ✅ Đã có aria-label (đã xác minh)
5. **Nút Hiện/Ẩn Mật Khẩu** (`apps/web/app/[locale]/(auth)/login/page.tsx:88-95`)
```tsx
```
- Trạng thái: ✅ Đã có aria-label (đã xác minh) - mặc dù cũng có văn bản hiển thị
6. **Nút Chuyển Đổi Hộp Thoại Lưu Tìm Kiếm** (`apps/web/app/[locale]/(public)/search/page.tsx`)
- **Vấn đề:** Nút chuyển đổi hộp thoại có thể chỉ có biểu tượng
- **Trạng thái:** Cần xác minh trong tệp đầy đủ
### Tóm Tắt Các Nút Chỉ Có Biểu Tượng
- **Được gắn nhãn đúng cách:** 10+ nút
- **Thiếu nhãn:** Các nút ảnh thu nhỏ trong thư viện hình ảnh (1 vị trí, nhiều nút)
- **Hành động khuyến nghị:** Thêm aria-label vào các nút ảnh thu nhỏ để hỗ trợ trình đọc màn hình tốt hơn
---
## 3. Ô NHẬP BIỂU MẪU KHÔNG CÓ NHÃN
### Các Thành Phần Biểu Mẫu Đã Phân Tích
#### ✅ Nhãn Được Liên Kết Đúng Cách
1. **Biểu Mẫu Đăng Nhập** (`apps/web/app/[locale]/(auth)/login/page.tsx`)
```tsx
{errors.phone && (
{errors.phone.message}
)}
```
- Trạng thái: ✅ Label với htmlFor, aria-describedby cho các lỗi
2. **Biểu Mẫu Đăng Ký** (`apps/web/app/[locale]/(auth)/register/page.tsx`)
- Họ tên đầy đủ, Số điện thoại, Email, Mật khẩu, Xác nhận mật khẩu
- Tất cả có thành phần Label với htmlFor
- Trạng thái: ✅ Được gắn nhãn đúng cách
3. **Biểu Mẫu Tìm Kiếm/Bộ Lọc** (`apps/web/components/search/filter-bar.tsx`)
```tsx
```
- Trạng thái: ✅ Sử dụng aria-label thay vì nhãn trực quan (chấp nhận được cho bộ lọc)
4. **Biểu Mẫu Định Giá** (`apps/web/components/valuation/valuation-form.tsx`)
```tsx