Skip to content

Commit 2811927

Browse files
committed
fix editfield focus and style
1 parent 6081f37 commit 2811927

2 files changed

Lines changed: 555 additions & 499 deletions

File tree

frontend/src/components/ui/EditableField.tsx

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { useState, useEffect } from "preact/hooks";
2-
import type { ComponentChildren } from "preact";
1+
import { useState, useEffect, useRef } from "preact/hooks";
2+
import type { ComponentChildren, JSX } from "preact";
33

44
type Props = {
55
value: string;
@@ -22,28 +22,83 @@ export function EditableField({
2222
}: Props) {
2323
const [isEditing, setIsEditing] = useState(false);
2424
const [tempValue, setTempValue] = useState(value);
25+
// Optimistic state to hold the value immediately after save
26+
const [optimisticValue, setOptimisticValue] = useState<string | null>(null);
2527

26-
useEffect(() => setTempValue(value), [value]);
28+
// References for DOM access and state tracking without re-renders
29+
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
30+
const isCanceling = useRef(false);
31+
32+
// Sync internal state if prop changes externally (e.g. WebSocket update)
33+
useEffect(() => {
34+
setTempValue(value);
35+
// When the server value updates, we can clear our optimistic override
36+
// because the UI is now consistent with the server (or the server rejected it).
37+
setOptimisticValue(null);
38+
}, [value]);
39+
40+
// Robust Focus Management
41+
useEffect(() => {
42+
if (isEditing && inputRef.current) {
43+
inputRef.current.focus();
44+
// Automatically select text for easy replacement (except for dates)
45+
if (
46+
type !== "datetime-local" &&
47+
inputRef.current instanceof HTMLInputElement
48+
) {
49+
inputRef.current.select();
50+
}
51+
}
52+
}, [isEditing, type]);
2753

2854
const handleSave = () => {
29-
setIsEditing(false);
30-
if (tempValue !== value) onSave(tempValue);
55+
// If we are in the middle of cancelling (Escape), do not save.
56+
if (isCanceling.current) {
57+
isCanceling.current = false;
58+
return;
59+
}
60+
61+
if (tempValue !== value) {
62+
// Optimistic Update: Show new value immediately
63+
setOptimisticValue(tempValue);
64+
onSave(tempValue);
65+
setIsEditing(false);
66+
} else {
67+
setIsEditing(false);
68+
}
69+
};
70+
71+
const handleKeyDown = (
72+
e: JSX.TargetedKeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
73+
) => {
74+
if (e.key === "Enter" && type !== "textarea") {
75+
e.currentTarget.blur();
76+
}
77+
if (e.key === "Escape") {
78+
isCanceling.current = true;
79+
setTempValue(value); // Revert input value
80+
setIsEditing(false);
81+
}
82+
};
83+
84+
const startEditing = () => {
85+
if (canEdit) {
86+
isCanceling.current = false;
87+
// Start editing with the visible value (optimistic or server),
88+
// so we don't revert to old text if the server is slow.
89+
setTempValue(optimisticValue !== null ? optimisticValue : value);
90+
setIsEditing(true);
91+
}
3192
};
3293

3394
if (isEditing && canEdit) {
3495
const commonProps = {
96+
ref: inputRef,
3597
class: className,
3698
value: tempValue,
3799
onInput: (e: any) => setTempValue(e.currentTarget.value),
38100
onBlur: handleSave,
39-
onKeyDown: (e: KeyboardEvent) => {
40-
if (e.key === "Enter" && type !== "textarea") handleSave();
41-
if (e.key === "Escape") {
42-
setTempValue(value);
43-
setIsEditing(false);
44-
}
45-
},
46-
autoFocus: true,
101+
onKeyDown: handleKeyDown,
47102
};
48103

49104
return type === "textarea" ? (
@@ -53,14 +108,19 @@ export function EditableField({
53108
);
54109
}
55110

111+
// Determine display value: Optimistic -> Server Value -> Placeholder
112+
const displayValue = optimisticValue !== null ? optimisticValue : value;
113+
56114
return (
57115
<div
58-
className={`${className} ${canEdit ? "editable" : ""} ${!value && !children ? "empty" : ""}`}
59-
onClick={() => canEdit && setIsEditing(true)}
116+
className={`${className} ${canEdit ? "editable" : ""} ${
117+
!value && !children ? "empty" : ""
118+
}`}
119+
onClick={startEditing}
60120
>
61121
{children
62122
? children
63-
: value || <span class="italic">{placeholder || "Empty"}</span>}
123+
: displayValue || <span class="italic">{placeholder || "Empty"}</span>}
64124
</div>
65125
);
66126
}

0 commit comments

Comments
 (0)