code-review-fix task 11.2: add role=dialog aria-modal and focus trapping to KeyboardShortcutsModal
This commit is contained in:
parent
6f7e3451a2
commit
9e4ad02f44
1 changed files with 66 additions and 2 deletions
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue