Skip to content

Commit a2a1b44

Browse files
authored
feat: support cleanup handler shortcut api (#27)
* add tsdoc about handler cleanup api in `KbsDefinition` * add shift+b example with cleanup method in dashboard Closes: #26
1 parent 8651b65 commit a2a1b44

File tree

3 files changed

+79
-29
lines changed

3 files changed

+79
-29
lines changed

src/app/Dashboard.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
import { useState } from 'react';
2+
13
import { useKbsGlobal } from '../component/index.ts';
24

35
import Playground from './Playground.tsx';
46
import { useCounter } from './useCounter.ts';
57

68
export default function Dashboard() {
79
const [counter, shortcuts] = useCounter({ maxFrequency: 2 });
8-
useKbsGlobal(shortcuts);
10+
const [color, setColor] = useState('white');
11+
useKbsGlobal([
12+
...shortcuts,
13+
{
14+
shortcut: { key: 'b', shift: true },
15+
handler() {
16+
setColor('black');
17+
18+
return () => setColor('white');
19+
},
20+
},
21+
]);
922
return (
10-
<div className="p-4 space-y-8">
23+
<div
24+
className={`p-4 space-y-8 ${color === 'white' ? 'bg-white text-black' : 'bg-black text-white'}`}
25+
>
1126
<p>
1227
Dashboard counter: {counter}. Press I or C to increment (max 2 per
1328
second if held down) and D to decrement.
1429
</p>
30+
<p>
31+
Press <kbd>Shift+B</kbd> to invert the background color of the page.
32+
Release <kbd>Shift</kbd> or <kbd>B</kbd> to reset.
33+
</p>
1534
<Playground />
1635
</div>
1736
);

src/component/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ export interface KbsKeyDefinitionCode extends KbsKeyDefinitionModifiers {
1616

1717
export type KbsKeyDefinition = KbsKeyDefinitionKey | KbsKeyDefinitionCode;
1818

19-
export type KbsHandler = (
19+
export type KbsHandlerCleaner = (
2020
event: KeyboardEvent<HTMLDivElement> | globalThis.KeyboardEvent,
2121
) => void;
22+
export type KbsHandler = (
23+
event: KeyboardEvent<HTMLDivElement> | globalThis.KeyboardEvent,
24+
) => KbsHandlerCleaner | void;
2225

2326
/**
2427
* Extend this interface to customize the metadata type.
@@ -36,6 +39,8 @@ export interface KbsDefinition {
3639
| ReadonlyArray<string | KbsKeyDefinition>;
3740
/**
3841
* The handler function to call when the shortcut is triggered.
42+
* If the handler returns a cleanup method (`KbsHandlerCleaner`),
43+
* it will listen for keyup events and call the cleanup method once a key related to the shortcut is released.
3944
*/
4045
handler: KbsHandler;
4146
/**

src/component/utils/get_key_down_handler.ts

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ export function useLastTriggerRef() {
1818
return useRef<LastTriggerData>({ keyOrCode: '', timestamp: 0 });
1919
}
2020

21+
function parseEvent(
22+
event: KeyboardEvent | ReactKeyboardEvent<HTMLDivElement>,
23+
combinedShortcuts: Record<string, KbsInternalShortcut>,
24+
) {
25+
const { key, code } = eventToKeyOrCode(event);
26+
let keyOrCode;
27+
let shortcut;
28+
if (combinedShortcuts[key]) {
29+
shortcut = combinedShortcuts[key];
30+
keyOrCode = key;
31+
} else {
32+
shortcut = combinedShortcuts[code];
33+
keyOrCode = code;
34+
}
35+
36+
return { key, code, keyOrCode, shortcut };
37+
}
38+
2139
export function getKeyDownHandler(
2240
lastTrigger: MutableRefObject<LastTriggerData>,
2341
combinedShortcuts: Record<string, KbsInternalShortcut>,
@@ -28,33 +46,41 @@ export function getKeyDownHandler(
2846
if (shouldIgnoreElement(event.target as HTMLElement)) {
2947
return;
3048
}
31-
const { key, code } = eventToKeyOrCode(event);
32-
let keyOrCode;
33-
let shortcut;
34-
if (combinedShortcuts[key]) {
35-
shortcut = combinedShortcuts[key];
36-
keyOrCode = key;
37-
} else {
38-
shortcut = combinedShortcuts[code];
39-
keyOrCode = code;
40-
}
41-
if (shortcut) {
42-
event.stopPropagation();
43-
event.preventDefault();
44-
45-
if (shortcut.maxFrequency > 0) {
46-
const now = performance.now();
47-
if (
48-
event.repeat &&
49-
lastTrigger.current.keyOrCode === keyOrCode &&
50-
now - lastTrigger.current.timestamp < 1000 / shortcut.maxFrequency
51-
) {
52-
return;
53-
}
54-
lastTrigger.current = { keyOrCode, timestamp: now };
55-
}
5649

57-
shortcut.handler(event);
50+
const { key, keyOrCode, shortcut } = parseEvent(event, combinedShortcuts);
51+
if (!shortcut) return;
52+
const initialKeys = new Set(key.split(']_'));
53+
54+
event.stopPropagation();
55+
event.preventDefault();
56+
57+
if (shortcut.maxFrequency > 0) {
58+
const now = performance.now();
59+
if (
60+
event.repeat &&
61+
lastTrigger.current.keyOrCode === keyOrCode &&
62+
now - lastTrigger.current.timestamp < 1000 / shortcut.maxFrequency
63+
) {
64+
return;
65+
}
66+
lastTrigger.current = { keyOrCode, timestamp: now };
5867
}
68+
69+
const cleanup = shortcut.handler(event);
70+
if (!cleanup) return;
71+
72+
const handleKeyUp = (
73+
event: KeyboardEvent | ReactKeyboardEvent<HTMLDivElement>,
74+
) => {
75+
if (shouldIgnoreElement(event.target as HTMLElement)) return;
76+
const { key } = parseEvent(event, combinedShortcuts);
77+
const releasedKeys = key.split(']_');
78+
if (!releasedKeys.some((key) => initialKeys.has(key))) return;
79+
80+
document.body.removeEventListener('keyup', handleKeyUp);
81+
cleanup(event);
82+
};
83+
84+
document.body.addEventListener('keyup', handleKeyUp);
5985
};
6086
}

0 commit comments

Comments
 (0)