OPERATOR

InlineEdit

Edit, validate and save

InlineEdit displays the controlled value until Edit opens its input. Save passes the exact draft to your adapter. Cancel returns to the supplied value. Casing is preserved throughout.

Alpha relay

Save requests an edit. Supply acceptance or rejection with the controls above. This example stores the accepted value locally.

EQ-01

In this example, Save waits for Accept pending edit or Reject pending edit. Rejection retains the draft and explains the failure. Acceptance replaces the displayed value with the supplied local result.

<InlineEdit
  label="Equipment name"
  value={name}
  validate={(value) => (value.trim() ? undefined : "Enter an equipment name.")}
  onSave={async (next) => {
    await saveName(next);
    setName(next);
  }}
/>

The adapter owns persistence and must reject when a save fails. The component does not infer backend acceptance or update the authoritative value itself. A resolved adapter closes the editor; the displayed value still comes from value.

Keyboard and focus

  • Edit focuses the input. Enter saves a single-line edit outside IME composition.
  • A supplied multiline value opens a textarea. Enter continues editing; choose Save to submit it.
  • Escape cancels outside pending and IME composition.
  • Save and Cancel have accessible names that include label, so multiple row editors remain distinguishable.
  • Cancel and successful Save return focus to Edit. Pending operations disable the input and both actions; when the adapter rejects, focus returns to the input with the draft and the error, unless the operator has moved on outside the editor.

Disabled, read-only and changed records

disabled keeps a disabled Edit button. readOnly displays the value without Edit. Revoking editability ends the editing session and invalidates pending UI work.

When value changes while the operator is typing (a sync arrived, another operator renamed the record), the editor stays open with the draft and focus stays in the input. A status line under the editor, also the input's description, names the new value: "Changed to “Alpha (synced)” while you were editing." Save submits the draft, and your adapter decides whether it replaces the newer value; Cancel shows the new value. A changed label is taken over the same way, without a status line.

While a save is pending, a value or label change that is neither the submitted draft nor the value the save started from ends the session; its later completion cannot overwrite the displayed value or reclaim focus. A supplied accepted value may update its label together and still receive the normal success focus return.

Use a stable React key for each record identity: a draft is kept across value and label changes, so a different record must get a different key, which starts a new editor without the draft. If a different record can have the same value and label, changing that key also distinguishes the new editor session. Scope your save adapter to the intended record and revision as well; the component can ignore stale UI completions but cannot cancel work already issued by the application.

Props

PROPTYPEDEFAULTREQUIRED
label

Names the editor and its Edit, Save and Cancel actions.

string—yes
value

Authoritative displayed value. Case is preserved. A change while the operator is typing keeps the draft and shows a status line naming the new value.

string—yes
onSave

Receives the exact draft; return a promise for asynchronous acceptance and reject to retain the draft with an error.

function—yes
validate

Returns an error message or undefined before saving.

function—no
disabledbooleanfalseno
readOnly

Displays the value without an Edit action.

booleanfalseno
classNamestring—no

The forwarded ref targets the outer div. Native div attributes pass through. validate returns a readable error string or undefined; it runs before the adapter. Adapter rejection keeps the draft and displays an associated error.

Accessibility

  • The editable control has the supplied accessible label and error association.
  • All three actions use native buttons with token focus indicators and at least 24px targets.
  • Validation and save failures have visible text and role="alert".
  • Pending feedback uses a status message and aria-busy. A value that changes while the operator types is announced by a status line that also describes the input. Both status lines are present, empty, for the whole edit and are filled when there is something to say, so screen readers announce them.
  • The value, error and action group wrap in narrow layouts.

Manifest entry

<InlineEdit label="Equipment name" value={name} onSave={saveName} validate={(value) => value.trim() ? undefined : "Enter a name."} />
View agent manifest
{
  "description": "Controlled inline text editing with explicit save, cancel and recoverable validation or save errors.",
  "props": {
    "label": {
      "type": "string",
      "required": true,
      "description": "Names the editor and its Edit, Save and Cancel actions."
    },
    "value": {
      "type": "string",
      "required": true,
      "description": "Authoritative displayed value. Case is preserved. A change while the operator is typing keeps the draft and shows a status line naming the new value."
    },
    "onSave": {
      "type": "function",
      "required": true,
      "description": "Receives the exact draft; return a promise for asynchronous acceptance and reject to retain the draft with an error."
    },
    "validate": {
      "type": "function",
      "description": "Returns an error message or undefined before saving."
    },
    "disabled": {
      "type": "boolean",
      "default": false
    },
    "readOnly": {
      "type": "boolean",
      "default": false,
      "description": "Displays the value without an Edit action."
    },
    "className": {
      "type": "string"
    }
  },
  "composable_with": [
    "ContextMenu",
    "DataGrid",
    "FormSection"
  ],
  "never_inside": [
    "Button"
  ],
  "example": "<InlineEdit label=\"Equipment name\" value={name} onSave={saveName} validate={(value) => value.trim() ? undefined : \"Enter a name.\"} />"
}

On this page