import { createEffect, createSignal, untrack } from 'solid-js'; import { defineWebComponent } from './define'; import { EditableLabel } from '../ui/editable-label'; interface Props extends Record { /** The label text — settable and reflected to the `value` attribute. Read * `el.value` for live state. */ value?: string; /** Controlled edit state. `el.editing = true` opens the field; reflected to the * `editing` attribute. */ editing?: boolean; /** Placeholder shown while editing / when the value is empty. */ placeholder?: string; /** Disable entering edit mode. */ disabled?: boolean; } /** Events fired by ``. */ interface Events { /** Committed a changed value (Enter / blur). */ 'kai-rename': { value: string }; /** Edit was cancelled (Esc); the text is restored. */ 'kai-cancel': Record; } /** * `` — inline rename, built on `kai-input`. Shows `value` as * text; double-click (or `el.edit()`, or `editing`) swaps in an autofocused * field. Enter or blur commits → `kai-rename` (only when the value changed); Esc * cancels → `kai-cancel` (the text is restored). * * ```html * * * ``` * * Methods: `edit()`, `commit()`, `cancel()`. Restyle via `::part(text)` and * `::part(input)`. */ defineWebComponent('kai-editable-label', { value: undefined, editing: false, placeholder: undefined, disabled: false, }, (props, ctx) => { const { element, dispatch, flag, expose } = ctx; // Controlled value, mirrored to the `value` attribute (the kai-segmented pattern). const [value, setValue] = createSignal( (props.value as string | undefined) ?? element.getAttribute('value') ?? '', ); const coerce = (v: unknown): string => v == null ? (element.getAttribute('value') ?? '') : String(v); Object.defineProperty(element, 'value', { get: () => value(), set: (v: unknown) => { const next = coerce(v); if (untrack(value) !== next) setValue(next); }, configurable: true, enumerable: true, }); createEffect(() => { const v = value(); if (v) { if (element.getAttribute('value') !== v) element.setAttribute('value', v); } else if (element.hasAttribute('value')) { element.removeAttribute('value'); } }); // Edit state lives in the facade and drives the primitive. The methods map to // the primitive's own gestures so every commit / cancel flows through its guarded // Enter/Esc/blur handlers (no double-fire). Controlled `editing` rides the native // solid-element prop/attribute: we follow it (so `el.editing = true` / // `` opens the field) and reflect it, but never // override the property with a side-effecting `Object.defineProperty` — doing so // fires the setter during solid-element's upgrade and corrupts the signal. const [editing, setEditing] = createSignal(flag('editing')); createEffect(() => setEditing(flag('editing'))); createEffect(() => { if (editing()) { if (!element.hasAttribute('editing')) element.setAttribute('editing', ''); } else if (element.hasAttribute('editing')) { element.removeAttribute('editing'); } }); const getInput = (): HTMLInputElement | null => element.shadowRoot?.querySelector('input') ?? null; const enter = () => { if (!flag('disabled')) setEditing(true); }; // Blur drives the primitive's blur-commit; Escape drives its cancel. const commit = () => { getInput()?.blur(); }; const cancel = () => { const input = getInput(); if (input) { input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, composed: true })); } else if (untrack(editing)) { setEditing(false); dispatch('kai-cancel', {}); } }; expose({ edit: enter, commit, cancel }); return ( <> { setEditing(false); if (next !== untrack(value)) { setValue(next); dispatch('kai-rename', { value: next }); } }} onCancel={() => { setEditing(false); dispatch('kai-cancel', {}); }} /> ); });