1+ "use client"
2+
3+ import { useEffect , useCallback , useMemo } from 'react'
4+ import { useRouter } from 'next/navigation'
5+
6+ interface KeyboardShortcut {
7+ key : string
8+ ctrlKey ?: boolean
9+ altKey ?: boolean
10+ shiftKey ?: boolean
11+ action : ( ) => void
12+ description : string
13+ }
14+
15+ interface KeyboardNavigationProps {
16+ shortcuts ?: KeyboardShortcut [ ]
17+ }
18+
19+ export function KeyboardNavigation ( { shortcuts = [ ] } : KeyboardNavigationProps ) {
20+ const router = useRouter ( )
21+
22+ // Default keyboard shortcuts for common actions
23+ const defaultShortcuts = useMemo < KeyboardShortcut [ ] > ( ( ) => [
24+ {
25+ key : 'h' ,
26+ altKey : true ,
27+ action : ( ) => router . push ( '/' ) ,
28+ description : 'Go to homepage'
29+ } ,
30+ {
31+ key : 'p' ,
32+ altKey : true ,
33+ action : ( ) => router . push ( '/projects' ) ,
34+ description : 'Go to projects page'
35+ } ,
36+ {
37+ key : 't' ,
38+ altKey : true ,
39+ action : ( ) => router . push ( '/team' ) ,
40+ description : 'Go to team page'
41+ } ,
42+ {
43+ key : 'c' ,
44+ altKey : true ,
45+ action : ( ) => router . push ( '/contact' ) ,
46+ description : 'Go to contact page'
47+ } ,
48+ {
49+ key : '/' ,
50+ action : ( ) => {
51+ // Focus on search if available, otherwise skip to main content
52+ const searchInput = document . querySelector ( 'input[type="search"]' ) as HTMLElement
53+ const mainContent = document . getElementById ( 'main-content' )
54+ if ( searchInput ) {
55+ searchInput . focus ( )
56+ } else if ( mainContent ) {
57+ mainContent . focus ( )
58+ }
59+ } ,
60+ description : 'Focus search or main content'
61+ }
62+ ] , [ router ] )
63+
64+ // Memoize all shortcuts to prevent recreation
65+ const allShortcuts = useMemo ( ( ) => {
66+ const showHelpShortcut : KeyboardShortcut = {
67+ key : '?' ,
68+ shiftKey : true ,
69+ action : ( ) => {
70+ showKeyboardShortcuts ( [ ...defaultShortcuts , ...shortcuts ] )
71+ } ,
72+ description : 'Show keyboard shortcuts'
73+ }
74+ return [ ...defaultShortcuts , ...shortcuts , showHelpShortcut ]
75+ } , [ defaultShortcuts , shortcuts ] )
76+
77+ const handleKeyDown = useCallback ( ( event : KeyboardEvent ) => {
78+ // Don't trigger shortcuts when typing in inputs
79+ const target = event . target as HTMLElement
80+ if ( target . tagName === 'INPUT' || target . tagName === 'TEXTAREA' || target . isContentEditable ) {
81+ // Allow some shortcuts even in inputs
82+ if ( event . key === 'Escape' ) {
83+ target . blur ( )
84+ return
85+ }
86+ return
87+ }
88+
89+ for ( const shortcut of allShortcuts ) {
90+ if (
91+ event . key . toLowerCase ( ) === shortcut . key . toLowerCase ( ) &&
92+ ! ! event . ctrlKey === ! ! shortcut . ctrlKey &&
93+ ! ! event . altKey === ! ! shortcut . altKey &&
94+ ! ! event . shiftKey === ! ! shortcut . shiftKey
95+ ) {
96+ event . preventDefault ( )
97+ shortcut . action ( )
98+ break
99+ }
100+ }
101+ } , [ allShortcuts ] )
102+
103+ useEffect ( ( ) => {
104+ document . addEventListener ( 'keydown' , handleKeyDown )
105+ return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown )
106+ } , [ handleKeyDown ] )
107+
108+ return null // This is a behavior-only component
109+ }
110+
111+ function showKeyboardShortcuts ( shortcuts : KeyboardShortcut [ ] ) {
112+ // Create a modal dialog showing keyboard shortcuts
113+ const modal = document . createElement ( 'div' )
114+ modal . className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'
115+ modal . setAttribute ( 'role' , 'dialog' )
116+ modal . setAttribute ( 'aria-modal' , 'true' )
117+ modal . setAttribute ( 'aria-labelledby' , 'shortcuts-title' )
118+
119+ const content = document . createElement ( 'div' )
120+ content . className = 'bg-background border border-border rounded-lg p-6 max-w-md mx-4 shadow-lg'
121+
122+ const title = document . createElement ( 'h2' )
123+ title . id = 'shortcuts-title'
124+ title . className = 'text-lg font-semibold mb-4'
125+ title . textContent = 'Keyboard Shortcuts'
126+
127+ const shortcutsList = document . createElement ( 'div' )
128+ shortcutsList . className = 'space-y-2 mb-4'
129+
130+ shortcuts . forEach ( shortcut => {
131+ const item = document . createElement ( 'div' )
132+ item . className = 'flex justify-between items-center text-sm'
133+
134+ const keys = document . createElement ( 'kbd' )
135+ keys . className = 'px-2 py-1 bg-muted rounded text-xs font-mono'
136+
137+ let keyText = ''
138+ if ( shortcut . ctrlKey ) keyText += 'Ctrl + '
139+ if ( shortcut . altKey ) keyText += 'Alt + '
140+ if ( shortcut . shiftKey ) keyText += 'Shift + '
141+ keyText += shortcut . key . toUpperCase ( )
142+
143+ keys . textContent = keyText
144+
145+ const description = document . createElement ( 'span' )
146+ description . className = 'text-muted-foreground'
147+ description . textContent = shortcut . description
148+
149+ item . appendChild ( keys )
150+ item . appendChild ( description )
151+ shortcutsList . appendChild ( item )
152+ } )
153+
154+ const closeButton = document . createElement ( 'button' )
155+ closeButton . className = 'w-full bg-primary text-primary-foreground px-4 py-2 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
156+ closeButton . textContent = 'Close'
157+ closeButton . onclick = ( ) => document . body . removeChild ( modal )
158+
159+ // Close on escape or click outside
160+ const closeModal = ( e : Event ) => {
161+ if ( e . target === modal || ( e as KeyboardEvent ) . key === 'Escape' ) {
162+ document . body . removeChild ( modal )
163+ document . removeEventListener ( 'keydown' , closeModal )
164+ }
165+ }
166+
167+ document . addEventListener ( 'keydown' , closeModal )
168+ modal . onclick = closeModal
169+ content . onclick = ( e ) => e . stopPropagation ( )
170+
171+ content . appendChild ( title )
172+ content . appendChild ( shortcutsList )
173+ content . appendChild ( closeButton )
174+ modal . appendChild ( content )
175+ document . body . appendChild ( modal )
176+
177+ // Focus the close button
178+ closeButton . focus ( )
179+ }
180+
181+ export type { KeyboardShortcut }
0 commit comments