feat: enhance accessibility across the application by improving keyboard navigation and screen reader support for various components
This commit is contained in:
@@ -26,6 +26,7 @@ function BellIcon({ className }: { className?: string }) {
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
@@ -83,16 +84,22 @@ export default function NotificationCenter() {
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
// * Close dropdown when clicking outside
|
||||
// * Close dropdown when clicking outside or pressing Escape
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
@@ -128,23 +135,32 @@ export default function NotificationCenter() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
aria-controls={open ? 'notification-dropdown' : undefined}
|
||||
className="relative p-2 text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white rounded-lg hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50 transition-colors"
|
||||
aria-label={unreadCount > 0 ? `${unreadCount} unread notifications` : 'Notifications'}
|
||||
aria-label={unreadCount > 0 ? `Notifications, ${unreadCount} unread` : 'Notifications'}
|
||||
>
|
||||
<BellIcon />
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-brand-orange rounded-full" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-brand-orange rounded-full" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100]">
|
||||
<div
|
||||
id="notification-dropdown"
|
||||
role="dialog"
|
||||
aria-label="Notifications"
|
||||
className="absolute right-0 top-full mt-2 w-80 sm:w-96 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-700 rounded-xl shadow-xl overflow-hidden z-[100]"
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h3 className="font-semibold text-neutral-900 dark:text-white">Notifications</h3>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleMarkAllRead}
|
||||
aria-label="Mark all notifications as read"
|
||||
className="text-sm text-brand-orange hover:underline"
|
||||
>
|
||||
Mark all read
|
||||
@@ -202,12 +218,10 @@ export default function NotificationCenter() {
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleNotificationClick(n)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleNotificationClick(n)}
|
||||
className={`block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer ${!n.read ? 'bg-brand-orange/5 dark:bg-brand-orange/10' : ''}`}
|
||||
className={`w-full text-left block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-800/50 cursor-pointer ${!n.read ? 'bg-brand-orange/5 dark:bg-brand-orange/10' : ''}`}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
{getTypeIcon(n.type)}
|
||||
@@ -225,7 +239,7 @@ export default function NotificationCenter() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
@@ -246,7 +260,7 @@ export default function NotificationCenter() {
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400 hover:text-brand-orange dark:hover:text-brand-orange transition-colors"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
<SettingsIcon className="w-4 h-4" aria-hidden="true" />
|
||||
Manage settings
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user