From 251e15a37dc171cbb6840079849f63f2d51db3d6 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Tue, 18 Nov 2025 14:58:03 +0800 Subject: [PATCH] feat: Enhance accessibility with ARIA attributes and focus trap implementation --- UI_AUDIT_REPORT.md | 32 +++++----- components/history/WatchHistorySidebar.tsx | 45 +++++++++++++- components/player/VideoPlayer.tsx | 7 ++- components/search/SearchForm.tsx | 5 ++ components/search/SearchHistoryDropdown.tsx | 5 ++ components/search/VideoGrid.tsx | 7 ++- lib/accessibility/focus-trap.ts | 69 +++++++++++++++++++++ 7 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 lib/accessibility/focus-trap.ts diff --git a/UI_AUDIT_REPORT.md b/UI_AUDIT_REPORT.md index 0c8c5d8..2d08c37 100644 --- a/UI_AUDIT_REPORT.md +++ b/UI_AUDIT_REPORT.md @@ -600,25 +600,25 @@ export const announceToScreenReader = (message: string) => { ### 🟡 High Priority (高优先级,2 周内完成) #### 5. **完善 ARIA 属性** (预计 3 小时) -- [ ] **SearchForm.tsx** - - [ ] 添加 `role="combobox"` - - [ ] 添加 `aria-expanded={showHistory}` - - [ ] 添加 `aria-controls="search-history-listbox"` - - [ ] 添加 `aria-autocomplete="list"` +- [✅] **SearchForm.tsx** + - [✅] 添加 `role="combobox"` + - [✅] 添加 `aria-expanded={showHistory}` + - [✅] 添加 `aria-controls="search-history-listbox"` + - [✅] 添加 `aria-autocomplete="list"` -- [ ] **WatchHistorySidebar.tsx** - - [ ] 添加 `role="complementary"` - - [ ] 添加 `aria-labelledby="history-sidebar-title"` - - [ ] 添加 `aria-hidden={!isOpen}` - - [ ] 实现焦点陷阱 (focus trap) +- [✅] **WatchHistorySidebar.tsx** + - [✅] 添加 `role="complementary"` + - [✅] 添加 `aria-labelledby="history-sidebar-title"` + - [✅] 添加 `aria-hidden={!isOpen}` + - [✅] 实现焦点陷阱 (focus trap) -- [ ] **VideoPlayer.tsx** - - [ ] 错误提示添加 `role="alert"` - - [ ] 添加 `aria-live="assertive"` +- [✅] **VideoPlayer.tsx** + - [✅] 错误提示添加 `role="alert"` + - [✅] 添加 `aria-live="assertive"` -- [ ] **VideoGrid.tsx** - - [ ] 添加 `role="list"` 到网格容器 - - [ ] 添加 `role="listitem"` 到每个卡片 +- [✅] **VideoGrid.tsx** + - [✅] 添加 `role="list"` 到网格容器 + - [✅] 添加 `role="listitem"` 到每个卡片 #### 6. **添加键盘导航支持** (预计 4 小时) - [ ] **VideoGrid.tsx** diff --git a/components/history/WatchHistorySidebar.tsx b/components/history/WatchHistorySidebar.tsx index b66abef..0267a4e 100644 --- a/components/history/WatchHistorySidebar.tsx +++ b/components/history/WatchHistorySidebar.tsx @@ -5,16 +5,50 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useHistoryStore } from '@/lib/store/history-store'; import { Icons } from '@/components/ui/Icon'; import { Button } from '@/components/ui/Button'; import { HistoryItem } from './HistoryItem'; import { HistoryEmptyState } from './HistoryEmptyState'; +import { trapFocus } from '@/lib/accessibility/focus-trap'; export function WatchHistorySidebar() { const [isOpen, setIsOpen] = useState(false); const { viewingHistory, removeFromHistory, clearHistory } = useHistoryStore(); + const sidebarRef = useRef(null); + const cleanupFocusTrapRef = useRef<(() => void) | null>(null); + + // Setup focus trap when sidebar opens + useEffect(() => { + if (isOpen && sidebarRef.current) { + cleanupFocusTrapRef.current = trapFocus(sidebarRef.current); + } + + return () => { + if (cleanupFocusTrapRef.current) { + cleanupFocusTrapRef.current(); + cleanupFocusTrapRef.current = null; + } + }; + }, [isOpen]); + + // Handle escape key to close sidebar + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('keydown', handleEscape); + } + + return () => { + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen]); return ( <> @@ -37,6 +71,10 @@ export function WatchHistorySidebar() { {/* Sidebar */}