import { useEffect, useRef } from 'react'; import { usePathname } from 'next/navigation'; /** * Focus Management Hook * * Manages focus behavior for accessibility: * - Moves focus to main heading on route changes * - Announces page changes to screen readers * - Restores focus after modals close */ /** * Focus the main heading (h1) on route change * * WCAG 2.4.3 Focus Order - ensures logical focus progression * Helps screen reader users understand page context after navigation */ export function useFocusOnRouteChange() { const pathname = usePathname(); const previousPathname = useRef(null); useEffect(() => { // Skip on initial mount if (previousPathname.current === null) { previousPathname.current = pathname; return; } // Only trigger if pathname actually changed if (previousPathname.current === pathname) { return; } previousPathname.current = pathname; // Small delay to ensure DOM is updated const timeoutId = setTimeout(() => { // Try to find the main heading (h1) const mainHeading = document.querySelector('h1'); if (mainHeading) { // Make the heading focusable temporarily const tabindex = mainHeading.getAttribute('tabindex'); if (tabindex === null) { mainHeading.setAttribute('tabindex', '-1'); } // Focus the heading with smooth scroll (mainHeading as HTMLElement).focus({ preventScroll: false }); // Remove tabindex if we added it if (tabindex === null) { // Keep tabindex=-1 for programmatic focus // This doesn't affect keyboard navigation but allows .focus() } } else { // Fallback: focus the main content area const main = document.getElementById('main-content'); if (main) { main.focus({ preventScroll: false }); } } // Announce page change to screen readers announcePageChange(pathname); }, 100); return () => clearTimeout(timeoutId); }, [pathname]); } /** * Announce route changes to screen readers */ function announcePageChange(pathname: string) { // Create a live region if it doesn't exist let liveRegion = document.getElementById('route-change-announcer'); if (!liveRegion) { liveRegion = document.createElement('div'); liveRegion.id = 'route-change-announcer'; liveRegion.setAttribute('role', 'status'); liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); liveRegion.className = 'sr-only'; // Screen reader only document.body.appendChild(liveRegion); } // Get page title from pathname const pageTitle = getPageTitle(pathname); // Update the announcement liveRegion.textContent = `Navigated to ${pageTitle}`; // Clear after announcement setTimeout(() => { if (liveRegion) { liveRegion.textContent = ''; } }, 1000); } /** * Get friendly page title from pathname */ function getPageTitle(pathname: string): string { const pathSegments = pathname.split('/').filter(Boolean); if (pathSegments.length === 0) return 'Home'; const pageMap: Record = { 'track': 'Track Activity', 'ai-assistant': 'AI Assistant', 'insights': 'Insights', 'analytics': 'Analytics', 'activities': 'Activities', 'children': 'Children', 'family': 'Family', 'settings': 'Settings', 'login': 'Login', 'register': 'Register', 'forgot-password': 'Forgot Password', 'reset-password': 'Reset Password', 'onboarding': 'Welcome', }; const lastSegment = pathSegments[pathSegments.length - 1]; return pageMap[lastSegment] || lastSegment.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); } /** * Focus trap for modals/dialogs * Returns to previously focused element when modal closes */ export function useFocusTrap(isOpen: boolean) { const previousFocus = useRef(null); useEffect(() => { if (isOpen) { // Store currently focused element previousFocus.current = document.activeElement as HTMLElement; } else { // Restore focus when modal closes if (previousFocus.current && typeof previousFocus.current.focus === 'function') { setTimeout(() => { previousFocus.current?.focus(); }, 0); } } }, [isOpen]); return previousFocus; } /** * Focus notification/toast when it appears * Useful for important messages that need immediate attention */ export function useFocusOnNotification(isVisible: boolean, notificationRef: React.RefObject) { useEffect(() => { if (isVisible && notificationRef.current) { // Small delay to ensure notification is rendered const timeoutId = setTimeout(() => { if (notificationRef.current) { notificationRef.current.focus(); } }, 100); return () => clearTimeout(timeoutId); } }, [isVisible, notificationRef]); }