1import { type ChangeEvent, type KeyboardEvent, useState } from "react";
2
3/**
4 * Buffer text input locally so keystrokes don't cause a settings-wide
5 * re-render (and optionally a server RPC round-trip) on every character.
6 * `onCommit` fires on blur and on Enter.
7 *
8 * The draft resynchronizes from the upstream `value` only when the input
9 * is not focused, so an external push (e.g. an optimistic settings
10 * update from the user's own commit, or a reset to defaults) doesn't
11 * clobber an in-progress edit.
12 *
13 * Returns a bag of props that should be spread onto an `<Input>`:
14 *
15 * const bag = useCommitOnBlur(instance.displayName ?? "", (next) => {...});
16 * <Input {...bag} placeholder="e.g. Work" />
17 */
18export function useCommitOnBlur(value: string, onCommit: (next: string) => void) {
19 const [draft, setDraft] = useState<string | null>(null);
20
21 return {
22 value: draft ?? value,
23 onChange: (event: ChangeEvent<HTMLInputElement>) => {
24 setDraft(event.target.value);
25 },
26 onFocus: () => {
27 setDraft(value);
28 },
29 onBlur: () => {
30 const next = draft ?? value;
31 setDraft(null);
32 if (next !== value) {
33 onCommit(next);
34 }
35 },
36 onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => {
37 if (event.nativeEvent.isComposing || event.keyCode === 229) return;
38 if (event.key === "Enter") {
39 event.preventDefault();
40 (event.target as HTMLInputElement).blur();
41 }
42 },
43 };
44}