@remit/ui 0.0.80 → 0.0.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/app-shell-types.ts +22 -0
- package/src/components/app-shell.tsx +5 -0
- package/src/components/keyboard-hint-bar.render.test.ts +29 -0
- package/src/components/keyboard-hint-bar.tsx +2 -14
- package/src/components/message-list-pane.stories.tsx +48 -35
- package/src/components/message-list-pane.tsx +82 -4
- package/src/components/message-row.tsx +22 -2
- package/src/components/self-update.ts +3 -4
- package/src/index.ts +30 -2
- package/src/lib/keymap-dispatch.test.ts +255 -0
- package/src/lib/keymap-dispatch.ts +244 -0
- package/src/lib/keymap.test.ts +68 -0
- package/src/lib/keymap.ts +241 -0
- package/src/lib/use-list-cursor.test.ts +273 -0
- package/src/lib/use-list-cursor.ts +263 -0
- package/src/lib/use-list-keyboard.test.ts +172 -0
- package/src/lib/use-list-keyboard.ts +101 -0
- package/src/lib/use-triage-keyboard.test.ts +153 -0
- package/src/lib/use-triage-keyboard.ts +119 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { TriageHandlers } from "./keymap.js";
|
|
3
|
+
import {
|
|
4
|
+
dispatchKey,
|
|
5
|
+
isControlTarget,
|
|
6
|
+
isEditableTarget,
|
|
7
|
+
type SequencePrefix,
|
|
8
|
+
} from "./keymap-dispatch.js";
|
|
9
|
+
|
|
10
|
+
interface UseTriageKeyboardOptions {
|
|
11
|
+
handlers: TriageHandlers;
|
|
12
|
+
/** Disable the whole layer (e.g. a blocking modal owns the keyboard). */
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The element the layer listens on. Omitted, the layer is the page's and
|
|
16
|
+
* takes the window — the app's. A layer belonging to one mounted surface
|
|
17
|
+
* passes that surface's element, so several of them on a page (a Storybook
|
|
18
|
+
* docs page carrying every list story) each answer only the keys pressed
|
|
19
|
+
* inside their own; `null` until that element is up, and inert until then.
|
|
20
|
+
*/
|
|
21
|
+
target?: HTMLElement | null;
|
|
22
|
+
/**
|
|
23
|
+
* Reset window (ms) for a pending `g …` sequence prefix. After this with no
|
|
24
|
+
* second key, the prefix is dropped. ~1s per the spec.
|
|
25
|
+
*/
|
|
26
|
+
sequenceTimeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Global keydown handler for the triage layer's VERBS (#429). Routes keystrokes
|
|
31
|
+
* through the pure {@link dispatchKey} core to the supplied handler table,
|
|
32
|
+
* staying fully inert while focus is in an editable surface (input/textarea/CE;
|
|
33
|
+
* even Esc is left to the focused field's own handler) and carrying the `g …`
|
|
34
|
+
* go-to sequence prefix across keystrokes with a timeout.
|
|
35
|
+
*
|
|
36
|
+
* List navigation and selection route through here and nowhere else: the list
|
|
37
|
+
* publishes its commands upward and its host wires them into the handler
|
|
38
|
+
* table, so `keymap.ts` is the source of truth for both the displayed bindings
|
|
39
|
+
* and the routed ones. Every surface that mounts a list — the app and the
|
|
40
|
+
* Storybook prototype — drives it from here, so the interaction reviewed in
|
|
41
|
+
* Storybook is the one users get. The list used to run a second window listener
|
|
42
|
+
* claiming the same keys, which is what made Enter unusable on every focused
|
|
43
|
+
* button in the app (#43).
|
|
44
|
+
*
|
|
45
|
+
* Other window-level keydown listeners still exist for keys this layer does not
|
|
46
|
+
* own — `?` at the mail layout, `/` in SearchBar, Esc in the compose and
|
|
47
|
+
* conversation views. They bind disjoint keys; only the list's competing
|
|
48
|
+
* listener was removed.
|
|
49
|
+
*
|
|
50
|
+
* Per-action targeting (focused row vs selection) and the actual mutations live
|
|
51
|
+
* in the handlers the caller passes in — this hook only dispatches.
|
|
52
|
+
*/
|
|
53
|
+
export function useTriageKeyboard({
|
|
54
|
+
handlers,
|
|
55
|
+
enabled = true,
|
|
56
|
+
target,
|
|
57
|
+
sequenceTimeoutMs = 1000,
|
|
58
|
+
}: UseTriageKeyboardOptions): void {
|
|
59
|
+
// Latest handlers without re-subscribing the listener every render.
|
|
60
|
+
const handlersRef = useRef(handlers);
|
|
61
|
+
handlersRef.current = handlers;
|
|
62
|
+
|
|
63
|
+
const prefixRef = useRef<SequencePrefix>(null);
|
|
64
|
+
const prefixTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!enabled) return;
|
|
68
|
+
|
|
69
|
+
const clearPrefixTimer = () => {
|
|
70
|
+
if (prefixTimerRef.current !== null) {
|
|
71
|
+
clearTimeout(prefixTimerRef.current);
|
|
72
|
+
prefixTimerRef.current = null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
77
|
+
const result = dispatchKey(
|
|
78
|
+
{
|
|
79
|
+
key: event.key,
|
|
80
|
+
shiftKey: event.shiftKey,
|
|
81
|
+
metaKey: event.metaKey,
|
|
82
|
+
ctrlKey: event.ctrlKey,
|
|
83
|
+
altKey: event.altKey,
|
|
84
|
+
inEditable: isEditableTarget(event.target),
|
|
85
|
+
onControl: isControlTarget(event.target),
|
|
86
|
+
},
|
|
87
|
+
prefixRef.current,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Update the pending prefix and (re)arm / clear its reset timer.
|
|
91
|
+
clearPrefixTimer();
|
|
92
|
+
prefixRef.current = result.nextPrefix;
|
|
93
|
+
if (result.nextPrefix !== null) {
|
|
94
|
+
prefixTimerRef.current = setTimeout(() => {
|
|
95
|
+
prefixRef.current = null;
|
|
96
|
+
prefixTimerRef.current = null;
|
|
97
|
+
}, sequenceTimeoutMs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (result.action === null) return;
|
|
101
|
+
|
|
102
|
+
const handler = handlersRef.current[result.action];
|
|
103
|
+
if (!handler) return;
|
|
104
|
+
|
|
105
|
+
if (result.preventDefault) event.preventDefault();
|
|
106
|
+
handler();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const element: EventTarget | null = target === undefined ? window : target;
|
|
110
|
+
if (element === null) return;
|
|
111
|
+
|
|
112
|
+
element.addEventListener("keydown", onKeyDown as EventListener);
|
|
113
|
+
return () => {
|
|
114
|
+
element.removeEventListener("keydown", onKeyDown as EventListener);
|
|
115
|
+
clearPrefixTimer();
|
|
116
|
+
prefixRef.current = null;
|
|
117
|
+
};
|
|
118
|
+
}, [enabled, sequenceTimeoutMs, target]);
|
|
119
|
+
}
|