Skróty klawiszowe
View on GitHubZarejestruj skrót zakresu komponentu
Przećwiczony przykład: dodanie skrótu n na stronie listy projektów, który otwiera przepływ "nowy projekt" — scope'owany do tej strony, w przeciwieństwie do mod+k, który powinien działać wszędzie (zobacz 02-register-a-global-shortcut.mdSkróty klawiszoweZarejestruj skrót globalnyPrzećwiczony przykład: dodanie alt+i dla "Go to my Issues" (strona, która jeszcze nie ma trasy, użyta czysto do zademonstrowania kształtu rejestracji) — sk… po ten kształt zamiast tego). To odzwierciedla dokładnie prawdziwy skrót p → "Create project" w Sidebar.tsx.
Format stringa key
| Format | Przykład | Znaczenie |
|---|---|---|
| Pojedynczy znak | 'p', '?' | Naciśnięty samodzielnie, bez trzymanego modyfikatora |
modyfikator+klawisz | 'ctrl+k', 'alt+p' | Trzymany modyfikator + klawisz, sprawdzany względem flag eventu ctrlKey/altKey/metaKey/shiftKey |
mod+klawisz | 'mod+k' | mod to meta (⌘) na macOS i ctrl gdzie indziej; chipy w pomocy pokazują ⌘ lub CTRL odpowiednio. Preferuj go zamiast na sztywno wpisanego ctrl+… dla skrótów w całej aplikacji |
| Kombinacje Alt/Option | 'alt+p' | Dopasowywane po fizycznym klawiszu (event.code), bo Option na macOS zmienia event.key (Option+P → π) |
| Sekwencja rozdzielona spacją | 'g p' | Dwa klawisze bez modyfikatora naciśnięte w ciągu 500ms od siebie (śledzone "combo" przez comboRef) — używają tego globalne komendy nawigacji (g d Dashboard, g p Projects, g s Settings, g n Notifications); wygrywa najdłuższy ciąg ostatnich klawiszy, więc g p bije jednoklawiszowe p |
Zawsze małymi literami — ShortcutContext normalizuje zarówno zarejestrowany key, jak i naciśniętą kombinację do małych liter przed porównaniem, więc 'Ctrl+K' i 'ctrl+k' zachowują się identycznie, ale zapisz go małymi literami dla spójności z każdą istniejącą definicją.
Jedna definicja, kilka klawiszy i scope'y
key może być tablicą — key: ['?', '/'], key: ['c', 'ctrl+i'] — więc jedna akcja to jedna definicja (i jeden wiersz w modalu pomocy), niezależnie od liczby bindingów.
scope?: 'global' | 'project' | 'issue' (domyślnie 'global') rozstrzyga, która definicja wygrywa, gdy kilka dzieli ten sam klawisz: uruchamiany jest najbardziej szczegółowy scope (issue > project > global), niezależnie od kolejności rejestracji. Wyłączona definicja jest pomijana, więc przejmuje następny scope niżej. Zbindowanie tego samego klawisza dwa razy w tym samym scope loguje console.warn (tylko w dev) z opisami obu definicji.
Nie binduj ctrl+f/mod+f — to natywne wyszukiwanie przeglądarki.
Krok — Zarejestruj skrót przez useShortcuts()
Plik: resources/js/Pages/Projects/Index.tsx
import { useShortcuts } from '@/context/ShortcutContext';
import { ShortcutDefinition } from '@/types/Shortcuts';
import { useMemo, useState } from 'react';
export default function ProjectsIndex({ projects }: ProjectsIndexProps) {
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
const shortcuts = useMemo(
(): ShortcutDefinition[] => [
{
key: 'n',
description: 'New project',
category: 'Creation',
action: () => setIsNewProjectModalOpen(true),
},
],
[],
);
useShortcuts(shortcuts);
// ...rest of the component...
}
useMemo tutaj to nie opcjonalna dekoracja — wewnętrzny useEffect useShortcuts() ponownie rejestruje (wyrejestruj starą tablicę, zarejestruj nową) za każdym razem, gdy zmienia się referencja tablicy definitions, a świeży literał [] przy każdym renderze inaczej wyrejestrowywałby/rejestrował ponownie przy każdym pojedynczym renderze strony. Jeśli action skrótu musi domykać się nad zmieniającym się stanem, umieść ten stan w tablicy zależności useMemo zamiast rezygnować z memoizacji — zobacz własny useMemo shortcuts w Sidebar.tsx (puste deps, ponieważ jego akcja wywołuje tylko stabilną funkcję setState) po bazowy kształt do skopiowania.
Rejestracja/czyszczenie są automatyczne: useShortcuts() rejestruje przy montowaniu i wyrejestrowuje przy odmontowaniu (albo za każdym razem, gdy zmienia się definitions) przez własny wewnętrzny useEffect — nie ma żadnego ręcznego czyszczenia do napisania w miejscu wywołania, w przeciwieństwie do wywoływania register()/registerBatch() bezpośrednio (co robi przewodnik 2, ponieważ musi uruchomić się raz na poziomie providera, nie per komponent).
category: 'Creation' umieszcza go w pasującej sekcji modalu pomocy, obok każdego innego skrótu z kategorią 'Creation' — wybierz z istniejącej unii ShortcutDefinition['category'] ('Navigation' | 'Creation' | 'Search' | 'View' | 'Action') zamiast wymyślać nową, chyba że skrót faktycznie nie pasuje do żadnej istniejącej kategorii.
Testy
resources/js/context/ShortcutContext.test.tsx— żadna zmiana nie jest potrzebna dla nowego miejsca wywołania; już pokrywaregister()/useShortcuts()generycznie, włącznie z faktyczną logiką dopasowania keydown.- Dowolny test pokrywający
Pages/Projects/Index.tsx(stwórz, jeśli nie istnieje) — nie symuluj prawdziwego eventukeydown; zamockuj zamiast tegouseShortcutsi asercuj względem tablicy definicji, z jaką został wywołany, dokładnie ten wzorzec, jakiego już używa test'registers a "p" shortcut that opens the new project modal'wresources/js/Components/Organisms/Sidebar/Sidebar.test.tsxdla własnego skrótu:
To testuje, że komponent rejestruje właściwą definicję i że jegoconst mockUseShortcuts = vi.hoisted(() => vi.fn()); vi.mock('@/context/ShortcutContext', () => ({ useShortcuts: mockUseShortcuts, })); test('registers an "n" shortcut that opens the new project modal', () => { render(<ProjectsIndex projects={[]} />); const shortcuts = mockUseShortcuts.mock.calls[0][0]; const newProjectShortcut = shortcuts.find( (s: { key: string }) => s.key === 'n', ); expect(newProjectShortcut).toBeDefined(); act(() => { newProjectShortcut.action(); }); expect(screen.getByTestId('new-project-modal')).toBeInTheDocument(); });actionrobi właściwą rzecz — celowo nie testuje ponownie własnej logiki dopasowania/normalizacji kluczaShortcutContext, którą już posiadaShortcutContext.test.tsx.
