-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathKeyboardShortcuts.tsx
More file actions
124 lines (107 loc) · 3.07 KB
/
KeyboardShortcuts.tsx
File metadata and controls
124 lines (107 loc) · 3.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import { useCallback, useEffect } from 'preact/hooks'
import {
useThemeStore,
useUtilityBarStore,
} from '../features/utility-bar/store'
import { usePostMessageReceiver } from '../features/iframe-postmessage/receiver'
import { isKeyboardEventMessage } from '../features/iframe-postmessage/messages'
/**
* A simplified version of a keyboard event.
* preventDefault is optional so we can safely
* serialize it in a postMessage
*/
export interface SimpleKeyboardEvent {
key: string
shiftKey: boolean
preventDefault?(): void
}
/**
* Finds the active element even in the shadow DOM.
*
* The app is rendered inside a parts-kit custom element with a shadow DOM.
* document.activeElement is the host element (<parts-kit>), not the inner <input>.
*/
function getDeepActiveElement(root: Document | ShadowRoot = document): Element | null {
// Start with the active element on the provided root (document by default)
let active: Element | null = (root as Document).activeElement ?? null
// If the active element hosts a shadow root, drill down to that shadow root's active element
while (active && (active as HTMLElement).shadowRoot) {
const shadow = (active as HTMLElement).shadowRoot as ShadowRoot
active = shadow.activeElement
}
return active
}
export function canHandleKeyboard(): boolean {
const active = getDeepActiveElement()
return !(
active instanceof HTMLInputElement ||
active instanceof HTMLTextAreaElement
)
}
/**
* Safely preventDefault when we can
*/
function preventDefault(e: SimpleKeyboardEvent) {
if (e.preventDefault !== undefined) {
e.preventDefault()
}
}
export default function () {
const keydownHandler = useCallback((e: SimpleKeyboardEvent) => {
const utilityStore = useUtilityBarStore.getState()
const themeStore = useThemeStore.getState()
if (!canHandleKeyboard()) {
return
}
switch (e.key.toLowerCase()) {
case 'f': {
preventDefault(e)
utilityStore.setIsNavBarVisible(!utilityStore.isNavBarVisible)
return
}
case 'escape': {
preventDefault(e)
utilityStore.setIsNavBarVisible(true)
return
}
case 'v': {
if (e.shiftKey) {
preventDefault(e)
utilityStore.setIsViewportOpen(!utilityStore.isViewportOpen)
}
return
}
case 't': {
if (e.shiftKey) {
preventDefault(e)
themeStore.toggleMode()
}
return
}
case 's': {
if (e.shiftKey) {
preventDefault(e)
utilityStore.setIsSettingsOpen(!utilityStore.isSettingsOpen)
}
return
}
default:
return
}
}, [])
usePostMessageReceiver({
onMessage(e) {
if (!isKeyboardEventMessage(e.data)) {
return
}
keydownHandler(e.data.payload)
},
})
useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => keydownHandler(event)
document.addEventListener('keydown', handleKeydown)
return () => {
document.removeEventListener('keydown', handleKeydown)
}
}, [keydownHandler])
}