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
44type 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