From d0d37b7846a67bc662b946e76a39965c610636ad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:08:31 +0000 Subject: [PATCH 1/4] Initial plan From 57a033022b78fb280078f755d974aa84e480ddfa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:18:57 +0000 Subject: [PATCH 2/4] Phase 1 Complete: Critical accessibility fixes implemented Co-authored-by: rezwana-karim <126201034+rezwana-karim@users.noreply.github.com> --- src/app/globals.css | 84 ++++++++++++++++++ src/components/layout/header.tsx | 141 +++++++++++++++++++++++-------- src/components/ui/button.tsx | 18 ++-- src/components/ui/skip-links.tsx | 14 +-- tailwind.config.ts | 1 + 5 files changed, 209 insertions(+), 49 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 152a468..f5a6267 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -148,3 +148,87 @@ body { * { border-color: var(--border); } + +/* Enhanced Focus Indicators for WCAG 2.1 AA Compliance */ +*:focus { + outline: none; +} + +*:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + border-radius: calc(var(--radius) - 2px); +} + +/* High contrast focus for better visibility */ +@media (prefers-contrast: high) { + *:focus-visible { + outline-width: 3px; + outline-offset: 3px; + } +} + +/* Skip links enhanced visibility */ +.sr-only:focus-visible, +.sr-only:focus { + position: absolute; + width: auto; + height: auto; + padding: 0.5rem 1rem; + margin: 0; + overflow: visible; + clip: auto; + white-space: nowrap; + background: var(--primary); + color: var(--primary-foreground); + border-radius: var(--radius); + box-shadow: var(--shadow-lg); + z-index: 1000; +} + +/* Button focus states */ +button:focus-visible, +[role="button"]:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; +} + +/* Link focus states */ +a:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} + +/* Form element focus states */ +input:focus-visible, +textarea:focus-visible, +select:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; + border-color: var(--ring); +} + +/* Ensure interactive elements have minimum touch target size */ +button, +[role="button"], +input[type="button"], +input[type="submit"], +input[type="reset"] { + min-height: 44px; + min-width: 44px; +} + +/* Improved link styling for better accessibility */ +a { + color: var(--primary); + text-decoration-skip-ink: auto; +} + +a:hover { + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 3px; +} diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index c292901..b968a4d 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -56,7 +56,11 @@ export function Header() {
{/* Logo */} - + @@ -80,22 +84,34 @@ export function Header() { {/* Community Dropdown */} - - + {navigationItems.community.map((item) => ( {item.label} @@ -108,22 +124,34 @@ export function Header() { {/* Resources Dropdown */} - - + {navigationItems.resources.map((item) => ( {item.label} @@ -137,13 +165,27 @@ export function Header() { {/* Mobile Menu Button */}
- - @@ -155,12 +197,12 @@ export function Header() { onClick={toggleMobileMenu} aria-expanded={isMobileMenuOpen} aria-controls="mobile-menu" - aria-label="Toggle navigation menu" + aria-label={isMobileMenuOpen ? "Close navigation menu" : "Open navigation menu"} > {isMobileMenuOpen ? ( - +
@@ -168,16 +210,21 @@ export function Header() { {/* Mobile Navigation Menu */} {isMobileMenuOpen && ( -
+ )} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index facc585..e51d3f9 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,18 +5,18 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80", - destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80", - outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-accent/80", - secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70", - ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80", - link: "text-primary underline-offset-4 hover:underline active:text-primary/80", - success: "bg-success text-success-foreground hover:bg-success/90 active:bg-success/80", - warning: "bg-warning text-warning-foreground hover:bg-warning/90 active:bg-warning/80", + default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80 focus-visible:ring-primary/70", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:bg-destructive/80 focus-visible:ring-destructive/70", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-accent/80 focus-visible:ring-accent/70", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary/70 focus-visible:ring-secondary/70", + ghost: "hover:bg-accent hover:text-accent-foreground active:bg-accent/80 focus-visible:ring-accent/70", + link: "text-primary underline-offset-4 hover:underline active:text-primary/80 focus-visible:ring-primary/70 focus-visible:ring-offset-0", + success: "bg-success text-success-foreground hover:bg-success/90 active:bg-success/80 focus-visible:ring-success/70", + warning: "bg-warning text-warning-foreground hover:bg-warning/90 active:bg-warning/80 focus-visible:ring-warning/70", }, size: { default: "h-10 px-4 py-2", diff --git a/src/components/ui/skip-links.tsx b/src/components/ui/skip-links.tsx index a9177d9..082b59f 100644 --- a/src/components/ui/skip-links.tsx +++ b/src/components/ui/skip-links.tsx @@ -8,21 +8,25 @@ const skipLinks = [ export function SkipLinks() { return ( -
+
{skipLinks.map((link) => ( diff --git a/tailwind.config.ts b/tailwind.config.ts index 6248d0c..f9af7ab 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,6 +6,7 @@ const config: Config = { "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", ], + darkMode: 'class', theme: { extend: { colors: { From 5b5d8444d73cb9f0f8f68aca75b9cf0ba2cc5636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Sep 2025 08:25:12 +0000 Subject: [PATCH 3/4] Phase 2 Complete: Advanced keyboard navigation & screen reader support Co-authored-by: rezwana-karim <126201034+rezwana-karim@users.noreply.github.com> --- src/app/layout.tsx | 4 +- src/app/page.tsx | 38 +++-- src/components/ui/keyboard-navigation.tsx | 181 ++++++++++++++++++++++ src/components/ui/live-region.tsx | 87 +++++++++++ src/components/ui/theme-toggle.tsx | 10 +- 5 files changed, 304 insertions(+), 16 deletions(-) create mode 100644 src/components/ui/keyboard-navigation.tsx create mode 100644 src/components/ui/live-region.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 186c4b6..a7ae2ef 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import "./globals.css"; import { Header } from "@/components/layout/header"; import { Footer } from "@/components/layout/footer"; import { SkipLinks } from "@/components/ui/skip-links"; +import { KeyboardNavigation } from "@/components/ui/keyboard-navigation"; import { ThemeProvider } from "@/lib/theme-context"; const geistSans = Geist({ @@ -57,10 +58,11 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > +
-
{children}
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 4aa1bec..5048525 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -33,13 +33,23 @@ export default function Home() { {/* CTA Buttons */} - @@ -47,9 +57,9 @@ export default function Home() { {/* Features Grid */}
- -
- +
+
+
Open Source @@ -57,10 +67,10 @@ export default function Home() { Building transparent, accessible solutions for the community
- +
- -
+
+
@@ -71,10 +81,10 @@ export default function Home() { Connecting passionate developers and researchers worldwide
- +
- -
+
+
@@ -85,7 +95,7 @@ export default function Home() { Pushing boundaries with cutting-edge research and development
- +
diff --git a/src/components/ui/keyboard-navigation.tsx b/src/components/ui/keyboard-navigation.tsx new file mode 100644 index 0000000..0faf835 --- /dev/null +++ b/src/components/ui/keyboard-navigation.tsx @@ -0,0 +1,181 @@ +"use client" + +import { useEffect, useCallback, useMemo } from 'react' +import { useRouter } from 'next/navigation' + +interface KeyboardShortcut { + key: string + ctrlKey?: boolean + altKey?: boolean + shiftKey?: boolean + action: () => void + description: string +} + +interface KeyboardNavigationProps { + shortcuts?: KeyboardShortcut[] +} + +export function KeyboardNavigation({ shortcuts = [] }: KeyboardNavigationProps) { + const router = useRouter() + + // Default keyboard shortcuts for common actions + const defaultShortcuts = useMemo(() => [ + { + key: 'h', + altKey: true, + action: () => router.push('/'), + description: 'Go to homepage' + }, + { + key: 'p', + altKey: true, + action: () => router.push('/projects'), + description: 'Go to projects page' + }, + { + key: 't', + altKey: true, + action: () => router.push('/team'), + description: 'Go to team page' + }, + { + key: 'c', + altKey: true, + action: () => router.push('/contact'), + description: 'Go to contact page' + }, + { + key: '/', + action: () => { + // Focus on search if available, otherwise skip to main content + const searchInput = document.querySelector('input[type="search"]') as HTMLElement + const mainContent = document.getElementById('main-content') + if (searchInput) { + searchInput.focus() + } else if (mainContent) { + mainContent.focus() + } + }, + description: 'Focus search or main content' + } + ], [router]) + + // Memoize all shortcuts to prevent recreation + const allShortcuts = useMemo(() => { + const showHelpShortcut: KeyboardShortcut = { + key: '?', + shiftKey: true, + action: () => { + showKeyboardShortcuts([...defaultShortcuts, ...shortcuts]) + }, + description: 'Show keyboard shortcuts' + } + return [...defaultShortcuts, ...shortcuts, showHelpShortcut] + }, [defaultShortcuts, shortcuts]) + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + // Don't trigger shortcuts when typing in inputs + const target = event.target as HTMLElement + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + // Allow some shortcuts even in inputs + if (event.key === 'Escape') { + target.blur() + return + } + return + } + + for (const shortcut of allShortcuts) { + if ( + event.key.toLowerCase() === shortcut.key.toLowerCase() && + !!event.ctrlKey === !!shortcut.ctrlKey && + !!event.altKey === !!shortcut.altKey && + !!event.shiftKey === !!shortcut.shiftKey + ) { + event.preventDefault() + shortcut.action() + break + } + } + }, [allShortcuts]) + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [handleKeyDown]) + + return null // This is a behavior-only component +} + +function showKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { + // Create a modal dialog showing keyboard shortcuts + const modal = document.createElement('div') + modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50' + modal.setAttribute('role', 'dialog') + modal.setAttribute('aria-modal', 'true') + modal.setAttribute('aria-labelledby', 'shortcuts-title') + + const content = document.createElement('div') + content.className = 'bg-background border border-border rounded-lg p-6 max-w-md mx-4 shadow-lg' + + const title = document.createElement('h2') + title.id = 'shortcuts-title' + title.className = 'text-lg font-semibold mb-4' + title.textContent = 'Keyboard Shortcuts' + + const shortcutsList = document.createElement('div') + shortcutsList.className = 'space-y-2 mb-4' + + shortcuts.forEach(shortcut => { + const item = document.createElement('div') + item.className = 'flex justify-between items-center text-sm' + + const keys = document.createElement('kbd') + keys.className = 'px-2 py-1 bg-muted rounded text-xs font-mono' + + let keyText = '' + if (shortcut.ctrlKey) keyText += 'Ctrl + ' + if (shortcut.altKey) keyText += 'Alt + ' + if (shortcut.shiftKey) keyText += 'Shift + ' + keyText += shortcut.key.toUpperCase() + + keys.textContent = keyText + + const description = document.createElement('span') + description.className = 'text-muted-foreground' + description.textContent = shortcut.description + + item.appendChild(keys) + item.appendChild(description) + shortcutsList.appendChild(item) + }) + + const closeButton = document.createElement('button') + closeButton.className = 'w-full bg-primary text-primary-foreground px-4 py-2 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2' + closeButton.textContent = 'Close' + closeButton.onclick = () => document.body.removeChild(modal) + + // Close on escape or click outside + const closeModal = (e: Event) => { + if (e.target === modal || (e as KeyboardEvent).key === 'Escape') { + document.body.removeChild(modal) + document.removeEventListener('keydown', closeModal) + } + } + + document.addEventListener('keydown', closeModal) + modal.onclick = closeModal + content.onclick = (e) => e.stopPropagation() + + content.appendChild(title) + content.appendChild(shortcutsList) + content.appendChild(closeButton) + modal.appendChild(content) + document.body.appendChild(modal) + + // Focus the close button + closeButton.focus() +} + +export type { KeyboardShortcut } \ No newline at end of file diff --git a/src/components/ui/live-region.tsx b/src/components/ui/live-region.tsx new file mode 100644 index 0000000..5032450 --- /dev/null +++ b/src/components/ui/live-region.tsx @@ -0,0 +1,87 @@ +"use client" + +import { useEffect, useRef } from 'react' + +interface LiveRegionProps { + children: React.ReactNode + priority?: 'polite' | 'assertive' + atomic?: boolean + className?: string +} + +export function LiveRegion({ + children, + priority = 'polite', + atomic = true, + className = "sr-only" +}: LiveRegionProps) { + const regionRef = useRef(null) + + return ( +
+ {children} +
+ ) +} + +// Hook for announcing messages to screen readers +export function useAnnouncement() { + const timeoutRef = useRef() + + const announce = (message: string, priority: 'polite' | 'assertive' = 'polite') => { + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + + // Create or find live region + let liveRegion = document.getElementById('live-announcements') + if (!liveRegion) { + liveRegion = document.createElement('div') + liveRegion.id = 'live-announcements' + liveRegion.setAttribute('aria-live', priority) + liveRegion.setAttribute('aria-atomic', 'true') + liveRegion.className = 'sr-only' + document.body.appendChild(liveRegion) + } + + // Update the aria-live attribute if priority changed + liveRegion.setAttribute('aria-live', priority) + + // Clear and then set the message + liveRegion.textContent = '' + + timeoutRef.current = setTimeout(() => { + if (liveRegion) { + liveRegion.textContent = message + } + }, 100) // Small delay ensures screen readers catch the change + } + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + } + } + }, []) + + return announce +} + +// Component for managing theme change announcements +export function ThemeAnnouncement({ theme }: { theme: 'light' | 'dark' }) { + const announce = useAnnouncement() + + useEffect(() => { + announce(`Switched to ${theme} mode`, 'polite') + }, [theme, announce]) + + return null +} \ No newline at end of file diff --git a/src/components/ui/theme-toggle.tsx b/src/components/ui/theme-toggle.tsx index f54e4e4..52e1d54 100644 --- a/src/components/ui/theme-toggle.tsx +++ b/src/components/ui/theme-toggle.tsx @@ -4,9 +4,17 @@ import React from 'react' import { SunIcon, MoonIcon } from '@radix-ui/react-icons' import { Button } from '@/components/ui/button' import { useTheme } from '@/hooks/use-theme' +import { useAnnouncement } from '@/components/ui/live-region' export function ThemeToggle() { const { theme, toggleTheme, mounted } = useTheme() + const announce = useAnnouncement() + + const handleToggle = () => { + toggleTheme() + const newTheme = theme === 'dark' ? 'light' : 'dark' + announce(`Switched to ${newTheme} mode`) + } if (!mounted) { return ( @@ -26,7 +34,7 @@ export function ThemeToggle() {