-
+
{
+ if (isFirstRoute.current) {
+ isFirstRoute.current = false;
+ return;
+ }
+ document.getElementById('main-content')?.focus();
+ }, [location.pathname]);
// Countdown timer for resend cooldown
useEffect(() => {
@@ -22,6 +35,16 @@ export default function Layout() {
return () => clearTimeout(timer);
}, [resendCooldown]);
+ // Close the mobile sidebar drawer with Escape
+ useEffect(() => {
+ if (!sidebarOpen) return;
+ const handleKey = (e) => {
+ if (e.key === 'Escape') setSidebarOpen(false);
+ };
+ document.addEventListener('keydown', handleKey);
+ return () => document.removeEventListener('keydown', handleKey);
+ }, [sidebarOpen]);
+
const handleResendVerification = async () => {
if (resendCooldown > 0 || resending) return;
setResending(true);
@@ -42,12 +65,20 @@ export default function Layout() {
return (
+ {/* Skip link — first tab stop, jumps past sidebar and navbar */}
+
+ {t('common.skipToContent')}
+
+
{/* Sidebar */}
setSidebarOpen(false)} />
{/* Main content */}
- setSidebarOpen(true)} />
+ setSidebarOpen(true)} sidebarOpen={sidebarOpen} />
{/* Email verification banner */}
{user && user.email_verified === 0 && (
@@ -67,7 +98,7 @@ export default function Layout() {
)}
-
+
@@ -77,6 +108,7 @@ export default function Layout() {
setSidebarOpen(false)}
+ aria-hidden="true"
/>
)}
diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx
index 045507b..7e3f863 100644
--- a/src/components/Modal.jsx
+++ b/src/components/Modal.jsx
@@ -1,33 +1,12 @@
-import { useEffect, useId, useRef } from 'react';
+import { useId } from 'react';
import { X } from 'lucide-react';
import { useLanguage } from '../contexts/LanguageContext';
+import useDialogA11y from '../hooks/useDialogA11y';
export default function Modal({ title, children, onClose, maxWidth = 'max-w-lg' }) {
const { t } = useLanguage();
const titleId = useId();
- const dialogRef = useRef(null);
- const previouslyFocused = useRef(null);
-
- // Keep the latest onClose in a ref so the mount-only effect below can call it
- // without listing onClose as a dependency. Callers pass a fresh inline arrow
- // each render; depending on it would re-run the effect on every keystroke and
- // steal focus back to the dialog (kicking the user out of input fields).
- const onCloseRef = useRef(onClose);
- onCloseRef.current = onClose;
-
- useEffect(() => {
- previouslyFocused.current = document.activeElement;
- const handleKey = (e) => {
- if (e.key === 'Escape') onCloseRef.current?.();
- };
- document.addEventListener('keydown', handleKey);
- // Focus the dialog so screen readers announce it and keyboard focus lands inside
- dialogRef.current?.focus();
- return () => {
- document.removeEventListener('keydown', handleKey);
- previouslyFocused.current?.focus?.();
- };
- }, []);
+ const dialogRef = useDialogA11y(true, onClose);
return (
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 804d6ff..f9aceb2 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -6,12 +6,13 @@ import { useState, useRef, useEffect } from 'react';
import api from '../services/api';
import NotificationBell from './NotificationBell';
-export default function Navbar({ onMenuClick }) {
+export default function Navbar({ onMenuClick, sidebarOpen }) {
const { user, logout } = useAuth();
const { t } = useLanguage();
const navigate = useNavigate();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
+ const dropdownButtonRef = useRef(null);
useEffect(() => {
function handleClick(e) {
@@ -23,6 +24,19 @@ export default function Navbar({ onMenuClick }) {
return () => document.removeEventListener('mousedown', handleClick);
}, []);
+ // Close the dropdown with Escape and return focus to its trigger
+ useEffect(() => {
+ if (!dropdownOpen) return;
+ function handleKey(e) {
+ if (e.key === 'Escape') {
+ setDropdownOpen(false);
+ dropdownButtonRef.current?.focus();
+ }
+ }
+ document.addEventListener('keydown', handleKey);
+ return () => document.removeEventListener('keydown', handleKey);
+ }, [dropdownOpen]);
+
const handleLogout = () => {
logout();
navigate('/');
@@ -45,6 +59,8 @@ export default function Navbar({ onMenuClick }) {