import { type JSX, Show, createSignal, createEffect, untrack } from 'solid-js';
import { cn } from '../utils/cn';
import { Input } from './input';
export interface EditableLabelProps {
/** The label text. */
value: string;
/** Controlled edit state. When it flips true the field opens; double-click and
* the host's `edit()` open it too. */
editing?: boolean;
/** Placeholder shown while editing (and as muted text when the value is empty). */
placeholder?: string;
/** Disable entering edit mode. */
disabled?: boolean;
/** Fires on commit (Enter / blur) with the new value — ONLY when it changed. */
onRename?: (value: string) => void;
/** Fires on Esc (cancel); the text is restored. */
onCancel?: () => void;
class?: string;
}
/**
* `EditableLabel`: inline rename. Shows `value` as text; double-click (or the
* `editing` prop, or a host `edit()`) swaps in an autofocused `Input` with the
* text pre-selected. Enter or blur commits (fires `onRename` only when the value
* changed); Esc cancels (fires `onCancel`, restores the text). Both exit editing.
*
* Parts: `text` (the read view), `input` (the field while editing).
*/
export function EditableLabel(props: EditableLabelProps): JSX.Element {
// `editing` is seeded from the controlled prop and follows it; double-click /
// commit / cancel toggle it locally without touching the prop (no feedback loop).
const [editing, setEditing] = createSignal(!!props.editing);
createEffect(() => setEditing(!!props.editing));
// The displayed text. Seeded from `value`, kept in sync with it, and updated
// optimistically on a successful rename so the read view shows the new label.
const [text, setText] = createSignal(props.value);
createEffect(() => setText(props.value));
let root: HTMLElement | undefined;
// Set before an Enter/Esc unmounts the field so its teardown blur does not
// double-commit (mirrors the AMUX inline-rename guard).
let suppressBlur = false;
const focusSelect = () => {
const input = root?.querySelector('input');
if (input) { input.focus(); input.select(); }
};
createEffect(() => { if (editing()) queueMicrotask(focusSelect); });
const enterEdit = () => { if (!props.disabled) setEditing(true); };
const commit = (next: string) => {
if (!untrack(editing)) return; // idempotent: a teardown blur after Enter is a no-op
setEditing(false);
if (next !== untrack(text)) { setText(next); props.onRename?.(next); }
};
const cancel = () => {
if (!untrack(editing)) return;
setEditing(false);
props.onCancel?.();
};
const onFieldKeyDown = (e: KeyboardEvent) => {
const el = e.currentTarget as HTMLInputElement;
if (e.key === 'Enter') { e.preventDefault(); suppressBlur = true; commit(el.value); }
else if (e.key === 'Escape') { e.preventDefault(); suppressBlur = true; cancel(); }
};
const onFieldBlur = (v: string) => {
if (suppressBlur) { suppressBlur = false; return; }
commit(v);
};
return (
{
if (!props.disabled && (e.key === 'Enter' || e.key === 'F2')) { e.preventDefault(); enterEdit(); }
}}
>
{text() || props.placeholder || ''}
}
>
);
}