Spaces:
Configuration error
Configuration error
Upload 2 files
Browse files- src/hooks/useDebounce.ts +17 -0
- src/hooks/useKeyboardShortcuts.ts +91 -0
src/hooks/useDebounce.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
|
| 3 |
+
export function useDebounce<T>(value: T, delay: number): T {
|
| 4 |
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
| 5 |
+
|
| 6 |
+
useEffect(() => {
|
| 7 |
+
const timer = setTimeout(() => {
|
| 8 |
+
setDebouncedValue(value);
|
| 9 |
+
}, delay);
|
| 10 |
+
|
| 11 |
+
return () => {
|
| 12 |
+
clearTimeout(timer);
|
| 13 |
+
};
|
| 14 |
+
}, [value, delay]);
|
| 15 |
+
|
| 16 |
+
return debouncedValue;
|
| 17 |
+
}
|
src/hooks/useKeyboardShortcuts.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useCallback, RefObject } from 'react';
|
| 2 |
+
|
| 3 |
+
export interface KeyboardShortcut {
|
| 4 |
+
key: string;
|
| 5 |
+
ctrl?: boolean;
|
| 6 |
+
shift?: boolean;
|
| 7 |
+
alt?: boolean;
|
| 8 |
+
handler: () => void;
|
| 9 |
+
description?: string;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[], enabled = true) {
|
| 13 |
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
| 14 |
+
if (!enabled) return;
|
| 15 |
+
if (
|
| 16 |
+
e.target instanceof HTMLInputElement ||
|
| 17 |
+
e.target instanceof HTMLTextAreaElement ||
|
| 18 |
+
(e.target as HTMLElement).isContentEditable
|
| 19 |
+
) {
|
| 20 |
+
return;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
for (const shortcut of shortcuts) {
|
| 24 |
+
const keyMatch = e.key.toLowerCase() === shortcut.key.toLowerCase() ||
|
| 25 |
+
e.code.toLowerCase() === shortcut.key.toLowerCase();
|
| 26 |
+
const ctrlMatch = shortcut.ctrl ? (e.ctrlKey || e.metaKey) : true;
|
| 27 |
+
const shiftMatch = shortcut.shift ? e.shiftKey : !e.shiftKey;
|
| 28 |
+
const altMatch = shortcut.alt ? e.altKey : !e.altKey;
|
| 29 |
+
|
| 30 |
+
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
|
| 31 |
+
e.preventDefault();
|
| 32 |
+
shortcut.handler();
|
| 33 |
+
break;
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}, [shortcuts, enabled]);
|
| 37 |
+
|
| 38 |
+
useEffect(() => {
|
| 39 |
+
window.addEventListener('keydown', handleKeyDown);
|
| 40 |
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
| 41 |
+
}, [handleKeyDown]);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function useFocusTrap<T extends HTMLElement>(ref: RefObject<T | null>, active: boolean) {
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
if (!active || !ref.current) return;
|
| 47 |
+
|
| 48 |
+
const focusableElements = ref.current.querySelectorAll<HTMLElement>(
|
| 49 |
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
| 50 |
+
);
|
| 51 |
+
const firstElement = focusableElements[0];
|
| 52 |
+
const lastElement = focusableElements[focusableElements.length - 1];
|
| 53 |
+
|
| 54 |
+
const handleTabKey = (e: KeyboardEvent) => {
|
| 55 |
+
if (e.key !== 'Tab') return;
|
| 56 |
+
|
| 57 |
+
if (e.shiftKey) {
|
| 58 |
+
if (document.activeElement === firstElement) {
|
| 59 |
+
e.preventDefault();
|
| 60 |
+
lastElement?.focus();
|
| 61 |
+
}
|
| 62 |
+
} else {
|
| 63 |
+
if (document.activeElement === lastElement) {
|
| 64 |
+
e.preventDefault();
|
| 65 |
+
firstElement?.focus();
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
ref.current.addEventListener('keydown', handleTabKey);
|
| 71 |
+
firstElement?.focus();
|
| 72 |
+
|
| 73 |
+
return () => {
|
| 74 |
+
ref.current?.removeEventListener('keydown', handleTabKey);
|
| 75 |
+
};
|
| 76 |
+
}, [ref, active]);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
export function announceToScreenReader(message: string, priority: 'polite' | 'assertive' = 'polite') {
|
| 80 |
+
const el = document.createElement('div');
|
| 81 |
+
el.setAttribute('role', 'status');
|
| 82 |
+
el.setAttribute('aria-live', priority);
|
| 83 |
+
el.setAttribute('aria-atomic', 'true');
|
| 84 |
+
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;';
|
| 85 |
+
document.body.appendChild(el);
|
| 86 |
+
|
| 87 |
+
setTimeout(() => {
|
| 88 |
+
el.textContent = message;
|
| 89 |
+
setTimeout(() => document.body.removeChild(el), 1000);
|
| 90 |
+
}, 100);
|
| 91 |
+
}
|