SafeRoute / src /hooks /useKeyboardShortcuts.ts
ayushsahu45's picture
Upload 2 files
833bc6e verified
import { useEffect, useCallback, RefObject } from 'react';
export interface KeyboardShortcut {
key: string;
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
handler: () => void;
description?: string;
}
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[], enabled = true) {
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (!enabled) return;
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
(e.target as HTMLElement).isContentEditable
) {
return;
}
for (const shortcut of shortcuts) {
const keyMatch = e.key.toLowerCase() === shortcut.key.toLowerCase() ||
e.code.toLowerCase() === shortcut.key.toLowerCase();
const ctrlMatch = shortcut.ctrl ? (e.ctrlKey || e.metaKey) : true;
const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
const altMatch = shortcut.alt ? e.altKey : !e.altKey;
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
e.preventDefault();
shortcut.handler();
break;
}
}
}, [shortcuts, enabled]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
}
export function useFocusTrap<T extends HTMLElement>(ref: RefObject<T | null>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return;
const focusableElements = ref.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
};
ref.current.addEventListener('keydown', handleTabKey);
firstElement?.focus();
return () => {
ref.current?.removeEventListener('keydown', handleTabKey);
};
}, [ref, active]);
}
export function announceToScreenReader(message: string, priority: 'polite' | 'assertive' = 'polite') {
const el = document.createElement('div');
el.setAttribute('role', 'status');
el.setAttribute('aria-live', priority);
el.setAttribute('aria-atomic', 'true');
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
document.body.appendChild(el);
setTimeout(() => {
el.textContent = message;
setTimeout(() => document.body.removeChild(el), 1000);
}, 100);
}