veza/apps/web/src/hooks/useGlobalKeyboardShortcuts.ts

136 lines
3.8 KiB
TypeScript

import { useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUIStore } from '@/stores/ui';
/**
* FE-COMP-022: Global keyboard shortcuts hook for common actions
*/
export interface GlobalKeyboardShortcutsOptions {
enabled?: boolean;
preventDefault?: boolean;
onHelpOpen?: () => void;
}
export function useGlobalKeyboardShortcuts(
options: GlobalKeyboardShortcutsOptions = {},
) {
const { enabled = true, preventDefault = true, onHelpOpen } = options;
const navigate = useNavigate();
const { setSidebarOpen, sidebarOpen } = useUIStore();
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (!enabled) return;
// Ignorer si l'utilisateur est en train de taper dans un input/textarea/contenteditable
const target = e.target as HTMLElement;
if (
target &&
(target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable === true ||
target.getAttribute('role') === 'textbox')
) {
// Permettre Ctrl+K même dans les inputs pour la recherche
if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
// Ctrl+K ou Cmd+K : Focus sur la recherche
if (preventDefault) {
e.preventDefault();
}
const searchInput = document.querySelector(
'input[type="search"], input[placeholder*="search" i], input[placeholder*="rechercher" i]',
) as HTMLInputElement;
if (searchInput) {
searchInput.focus();
searchInput.select();
} else {
// Sinon, naviguer vers la page de recherche
navigate('/search');
}
return;
}
return;
}
// Ctrl+K ou Cmd+K : Focus sur la recherche ou naviguer vers /search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
if (preventDefault) {
e.preventDefault();
}
const searchInput = document.querySelector(
'input[type="search"], input[placeholder*="search" i], input[placeholder*="rechercher" i]',
) as HTMLInputElement;
if (searchInput) {
searchInput.focus();
searchInput.select();
} else {
navigate('/search');
}
return;
}
// Ctrl+N ou Cmd+N : Nouveau message/chat
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
if (preventDefault) {
e.preventDefault();
}
navigate('/chat');
return;
}
// Ctrl+B ou Cmd+B : Toggle sidebar
if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
if (preventDefault) {
e.preventDefault();
}
setSidebarOpen(!sidebarOpen);
return;
}
// Escape : Fermer les modals/dropdowns ou retourner en arrière
if (e.key === 'Escape') {
// Fermer les dropdowns ouverts
const openDropdowns = document.querySelectorAll(
'[role="menu"][aria-expanded="true"]',
);
openDropdowns.forEach((dropdown) => {
const button = dropdown.previousElementSibling as HTMLElement;
if (button) {
button.click();
}
});
return;
}
// ? : Afficher l'aide des raccourcis
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
if (preventDefault) {
e.preventDefault();
}
if (onHelpOpen) {
onHelpOpen();
}
return;
}
},
[
enabled,
preventDefault,
navigate,
setSidebarOpen,
sidebarOpen,
onHelpOpen,
],
);
useEffect(() => {
if (!enabled) return;
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [enabled, handleKeyDown]);
}