diff --git a/CHANGELOG.md b/CHANGELOG.md index 42a7b4d..b9c956b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Organization context switch.** Switching away from a deleted organization now stores the session correctly instead of using an insecure fallback. - **Removed debug logs.** Auth and organization-switching details no longer leak into the browser console in production. - **Stricter type safety.** Eliminated all `any` types and `@ts-ignore` suppressions across the codebase, so the TypeScript compiler catches more bugs at build time. +- **Accessibility improvements.** The notification bell, workspace switcher, and all dashboard tabs are now fully keyboard-navigable. Screen readers announce unread counts, active organizations, and tab changes correctly. Decorative icons are hidden from assistive technology. ## [0.10.0-alpha] - 2026-02-21 diff --git a/components/WorkspaceSwitcher.tsx b/components/WorkspaceSwitcher.tsx index f77a1c7..81b828c 100644 --- a/components/WorkspaceSwitcher.tsx +++ b/components/WorkspaceSwitcher.tsx @@ -43,8 +43,8 @@ export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: Orga } return ( -
-
+
+ @@ -74,21 +74,28 @@ export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: Orga ))} @@ -98,7 +105,7 @@ export default function OrganizationSwitcher({ orgs, activeOrgId }: { orgs: Orga href="/onboarding" className="w-full flex items-center gap-2 px-3 py-2 text-sm text-neutral-500 hover:text-blue-600 dark:text-neutral-400 dark:hover:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/10 rounded-lg transition-colors mt-1" > -
+ Create Organization diff --git a/components/dashboard/ContentStats.tsx b/components/dashboard/ContentStats.tsx index 49f3363..42c8169 100644 --- a/components/dashboard/ContentStats.tsx +++ b/components/dashboard/ContentStats.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react' import { formatNumber } from '@ciphera-net/ui' +import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' import { TopPage, getTopPages, getEntryPages, getExitPages } from '@/lib/api/stats' import { Modal, ArrowUpRightIcon, LayoutDashboardIcon } from '@ciphera-net/ui' import { ListSkeleton } from '@/components/skeletons' @@ -22,6 +23,7 @@ const LIMIT = 7 export default function ContentStats({ topPages, entryPages, exitPages, domain, collectPagePaths = true, siteId, dateRange }: ContentStatsProps) { const [activeTab, setActiveTab] = useState('top_pages') + const handleTabKeyDown = useTabListKeyboard() const [isModalOpen, setIsModalOpen] = useState(false) const [fullData, setFullData] = useState([]) const [isLoadingFull, setIsLoadingFull] = useState(false) @@ -103,7 +105,7 @@ export default function ContentStats({ topPages, entryPages, exitPages, domain, )}
-
+
{(['top_pages', 'entry_pages', 'exit_pages'] as Tab[]).map((tab) => ( )}
-
+
{(['map', 'countries', 'regions', 'cities'] as Tab[]).map((tab) => ( )}
-
+
{(['browsers', 'os', 'devices', 'screens'] as Tab[]).map((tab) => ( {open && ( -
+ + )} ))} @@ -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" > - +
diff --git a/lib/hooks/useTabListKeyboard.ts b/lib/hooks/useTabListKeyboard.ts new file mode 100644 index 0000000..269407b --- /dev/null +++ b/lib/hooks/useTabListKeyboard.ts @@ -0,0 +1,28 @@ +'use client' + +import { useCallback } from 'react' + +/** + * Provides an onKeyDown handler for WAI-ARIA tab lists. + * Moves focus between sibling `[role="tab"]` buttons with Left/Right arrow keys. + */ +export function useTabListKeyboard() { + return useCallback((e: React.KeyboardEvent) => { + const target = e.currentTarget + const tabs = Array.from(target.querySelectorAll('[role="tab"]')) + const index = tabs.indexOf(e.target as HTMLElement) + if (index < 0) return + + let next: number | null = null + if (e.key === 'ArrowRight') next = (index + 1) % tabs.length + else if (e.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length + else if (e.key === 'Home') next = 0 + else if (e.key === 'End') next = tabs.length - 1 + + if (next !== null) { + e.preventDefault() + tabs[next].focus() + tabs[next].click() + } + }, []) +} diff --git a/lib/utils/notifications.tsx b/lib/utils/notifications.tsx index 9ed6c7c..b4441f1 100644 --- a/lib/utils/notifications.tsx +++ b/lib/utils/notifications.tsx @@ -22,7 +22,7 @@ export function formatTimeAgo(dateStr: string): string { */ export function getTypeIcon(type: string) { if (type.includes('down') || type.includes('degraded') || type.startsWith('billing_')) { - return + return