import { useEffect, useRef, useState, type HTMLAttributes, type KeyboardEvent, } from "react"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; export const LIST_ROW_TITLE_FIELD_CLASS = "h-8 w-full rounded-md border px-3 text-sm leading-8"; function useInlineTitleEdit(input: { title: string; onSave: (title: string) => Promise; }) { const [editing, setEditing] = useState(false); const [draftTitle, setDraftTitle] = useState(input.title); const [optimisticTitle, setOptimisticTitle] = useState(null); const inputRef = useRef(null); const skipBlurSaveRef = useRef(false); const displayTitle = optimisticTitle ?? input.title; useEffect(() => { if (optimisticTitle !== null && input.title === optimisticTitle) { setOptimisticTitle(null); } if (!editing && optimisticTitle === null) { setDraftTitle(input.title); } }, [editing, optimisticTitle, input.title]); useEffect(() => { if (editing) { inputRef.current?.focus(); inputRef.current?.select(); } }, [editing]); async function saveTitle() { const trimmed = draftTitle.trim(); if (!trimmed || trimmed === input.title) { setDraftTitle(input.title); setOptimisticTitle(null); setEditing(false); return; } setOptimisticTitle(trimmed); setEditing(false); try { await input.onSave(trimmed); } catch { setOptimisticTitle(null); setDraftTitle(input.title); } } function handleTitleKeyDown(event: KeyboardEvent) { if (event.key === "Enter") { event.preventDefault(); skipBlurSaveRef.current = true; void saveTitle(); } if (event.key === "Escape") { event.preventDefault(); skipBlurSaveRef.current = true; setDraftTitle(displayTitle); setEditing(false); } } function handleTitleBlur() { if (skipBlurSaveRef.current) { skipBlurSaveRef.current = false; return; } void saveTitle(); } return { editing, setEditing, draftTitle, setDraftTitle, displayTitle, inputRef, handleTitleKeyDown, handleTitleBlur, }; } interface InlineEditableProps { value: string; onSave: (title: string) => Promise; ariaLabel: string; disabled?: boolean; titleDragProps?: HTMLAttributes; displayDone?: boolean; className?: string; onDisplayTitleChange?: (title: string) => void; } export function InlineEditable({ value, onSave, ariaLabel, disabled = false, titleDragProps, displayDone = false, className, onDisplayTitleChange, }: InlineEditableProps) { const { editing, setEditing, draftTitle, setDraftTitle, displayTitle, inputRef, handleTitleKeyDown, handleTitleBlur, } = useInlineTitleEdit({ title: value, onSave }); useEffect(() => { onDisplayTitleChange?.(displayTitle); }, [displayTitle, onDisplayTitleChange]); if (editing) { return ( setDraftTitle(event.target.value)} onBlur={handleTitleBlur} onKeyDown={handleTitleKeyDown} aria-label={ariaLabel} className={cn( LIST_ROW_TITLE_FIELD_CLASS, "border-input bg-background py-0 shadow-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0", className, )} disabled={disabled} /> ); } return ( ); }