code-review-fix task 11.2: add role=dialog aria-modal and focus trapping to KeyboardShortcutsModal

This commit is contained in:
Marko Djordjevic 2026-02-18 20:31:24 +01:00
parent 6f7e3451a2
commit 9e4ad02f44

View file

@ -1,6 +1,6 @@
'use client';
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { X } from 'lucide-react';
interface KeyboardShortcutsModalProps {
@ -21,7 +21,19 @@ const shortcuts = [
{ key: '?', description: 'Show this help' },
];
const FOCUSABLE_SELECTORS = [
'a[href]',
'button:not([disabled])',
'textarea:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ');
export default function KeyboardShortcutsModal({ open, onClose }: KeyboardShortcutsModalProps) {
const dialogRef = useRef<HTMLDivElement>(null);
// Escape / ? key handler
useEffect(() => {
if (!open) return;
const handleKey = (e: KeyboardEvent) => {
@ -33,6 +45,49 @@ export default function KeyboardShortcutsModal({ open, onClose }: KeyboardShortc
return () => window.removeEventListener('keydown', handleKey);
}, [open, onClose]);
// Focus trapping
useEffect(() => {
if (!open) return;
const dialog = dialogRef.current;
if (!dialog) return;
// Move focus into the dialog on open
const firstFocusable = dialog.querySelector<HTMLElement>(FOCUSABLE_SELECTORS);
firstFocusable?.focus();
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
const focusableElements = Array.from(
dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)
).filter((el) => !el.closest('[disabled]'));
if (focusableElements.length === 0) {
e.preventDefault();
return;
}
const first = focusableElements[0];
const last = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
dialog.addEventListener('keydown', handleTabKey);
return () => dialog.removeEventListener('keydown', handleTabKey);
}, [open]);
if (!open) return null;
return (
@ -41,11 +96,20 @@ export default function KeyboardShortcutsModal({ open, onClose }: KeyboardShortc
onClick={onClose}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="keyboard-shortcuts-title"
className="bg-background border border-border rounded-lg shadow-xl w-96 p-5"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-foreground">Keyboard Shortcuts</h2>
<h2
id="keyboard-shortcuts-title"
className="text-sm font-semibold text-foreground"
>
Keyboard Shortcuts
</h2>
<button
onClick={onClose}
className="p-1 rounded text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"