Editable Cell
Inline-editable value with an async commit lifecycle — optimistic display, saving/saved/error status, and revert on a rejected write.
- Status
- Since
0.4.0- Accessibility pattern
- button-to-editor disclosure
Last updated
Click to edit. The commit is async — watch the saving indicator.
Install
Add Editable Cell from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/editable-cellThe same command installs the registry items it composes: @vegastack/auto-save-input, @vegastack/field-inline, @vegastack/select, @vegastack/spinner, @vegastack/use-announcer, @vegastack/use-inline-edit.
Usage
import { EditableCell } from "@/components/ui/editable-cell";
<EditableCell
value={deal.name}
label="Deal name"
onCommit={(name) => api.updateDeal({ name })}
/>;EditableCell composes FieldInline as its
text leaf and layers on what every optimistic inline edit needs beyond the edit
interaction itself: an idle → saving → saved | error status indicator (the
same AutoSaveStatus vocabulary AutoSaveInput
uses), conflict revert, and a typed per-type editor.
When onCommit returns a promise, the cell shows the committed value
optimistically with a saving indicator. On resolve it flips to saved; on reject
it reverts the display to value and politely announces the revert — the
version_conflict path every optimistic CRM edit needs.
Scope
What this component deliberately does not own, in the style of
DataList's scope table:
| Behaviour | Where it lives |
|---|---|
| Persistence, retries, conflict check | Host — onCommit fires once per commit; a rejected promise = revert |
| Debounced auto-save while typing | AutoSaveInput |
| The grid's roving focus model | The grid host — focusMode="managed" only removes this tab stop |
| Date / actor / currency editors | Apps, via the open custom editor contract |
Examples
Conflict revert
Every commit is rejected: the value snaps back and the revert is announced.
Select editor
Real inline editors are per-type: a stage field edits with a popover list, not
a text box. The select editor renders a Select
whose popover is the editor; commit fires on selection.
States
Managed focus (grid hosts)
Inside a grid, one tab stop per editable cell per row is an ergonomics
regression — the grid's roving focus model owns reachability. focusMode="managed"
removes the cell's own tab stop, and the host opens the editor from its keyboard
model through the controlled editing / onEditingChange pair:
<EditableCell
value={cell.value}
label={column.header}
focusMode="managed"
editing={isEditingThisCell}
onEditingChange={(editing) => grid.setEditingCell(editing ? cellId : null)}
status={grid.cellStatus(rowId, column.key)}
onCommit={(next) => grid.commitCell(rowId, column.key, next)}
/>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onCommit* | (next: string) => void | Promise<void> | — | Commit callback. Return a promise to engage the async status layer: the
cell shows the committed value optimistically with a saving indicator,
flips to saved on resolve, and on reject **reverts to value** and
announces the revert (the version_conflict path). Return void for
synchronous hosts. |
value* | string | — | The persisted value. The cell displays it, edits a draft of it, and reverts to it on a failed commit. |
className | string | — | Extra classes merged onto the cell root. |
disabled | boolean | false | Blocks editing; the display is dimmed and out of the tab order. |
editing | boolean | — | Controlled edit mode, forwarded to the underlying editor. Required in
practice for managed hosts (the grid opens the editor on Enter/F2); omit
for the built-in click / <kbd>Enter</kbd> / <kbd>Space</kbd> activation. |
editor | EditableCellEditor | { type: "text" } | The editor to open. See EditableCellEditor. |
focusMode | "managed" | "standalone" | "standalone" | Focus policy. standalone (a card, a property list) gives the cell its own
tab stop. managed removes it — the host's roving focus model owns
reachability and opens the editor through editing/onEditingChange. |
label | string | — | Accessible name for the value being edited (e.g. "Deal amount"). Falls
back the same way FieldInline does; the editor is never unnamed. |
onEditingChange | ((editing: boolean) => void) | — | Called when the cell wants to enter (true) or leave (false) edit mode. |
readOnly | boolean | false | Renders the value as plain non-interactive text with no edit affordance. |
ref | React.Ref<HTMLSpanElement> | — | Ref forwarded to the cell's root <span> (data-slot="editable-cell"). |
status | AutoSaveStatus | — | Controlled status override. Omit it to let the cell derive status from the
onCommit promise; pass it when the host owns the write lifecycle (a
grid's cellStatus). Uses AutoSaveStatus — the system's one vocabulary
for async field writes. |
Data attributes and CSS variables on EditableCell
| Attribute | Values |
|---|---|
data-focus-mode | mirrors a prop or state value |
data-slot | "editable-cell" | "editable-cell-status" |
data-status | mirrors a prop or state value |
Editor contract
The custom editor receives EditableCellEditorProps:
| Prop | Type | Default | Description |
|---|---|---|---|
cancel* | () => void | — | Leave edit mode without committing. |
commit* | (next: string) => void | — | Commit next and leave edit mode. No-ops the async layer when unchanged. |
value* | string | — | The value being edited (the optimistic value while a commit is in flight). |
Accessibility
- The display element is a
role="button"(viaFieldInline) — Enter or Space opens the editor, mirroring a mouse click; the editor is never unnamed (label→placeholder→ generic fallback). - Status changes are announced through a visually hidden polite live region — "Saving…", "Saved", "Save failed — value reverted". The indicator icons differ by shape, never by colour alone.
- Escape cancels, restores the prior value, and returns to the display element; a rejected commit reverts the value and announces it.
- In
focusMode="managed"the cell has no tab stop of its own — the host grid must own reachability and open the editor from its keyboard model.
| Key | Action |
|---|---|
| Enter / Space | Open the editor from the focused display element. |
| Enter | Commit the draft (text editor). |
| Escape | Cancel and restore the prior value. |
| Contract | States tested |
|---|---|
| Behaviour | display, editing, saving, saved, error, disabled, read-only, managed-focus |
| Accessibility | keyboard, labeled, status-announcement, semantic-html |
| Visual | default, hover, saving, saved, error, disabled |