# InlineInput

Inline editable text. Displays a value as inline content and swaps to an auto-sizing text input when entering edit mode. Designed to drop into any text context (tab title, heading, paragraph, table cell) without style customization — typography, color, and font family all inherit from the parent.

Unlike `TextInput`, `InlineInput` is intentionally **not** a form field. It manages its own commit lifecycle (`onSubmit` / `onCancel`) rather than participating in form validation.

## When to Use

- **Renameable items**: Tab titles, file names, board columns, list items
- **Edit-in-place fields**: Inline document titles, profile names, project labels
- **Lightweight string edits**: Cases where launching a modal or full form would be heavy

## When Not to Use

- **Form fields**: Use `TextInput` (with `<Form>` + `Field`) for inputs that need validation rules
- **Multi-line text**: Single-line only in this version (multiline planned)
- **Complex inputs**: Use a proper input component when you need masks, prefixes/suffixes, or rich validation

## Component

---

### Properties

- **`value`** `string` — Controlled value. When provided, the component is controlled.
- **`defaultValue`** `string` — Initial value for uncontrolled usage.
- **`onChange`** `(value: string) => void` — Fires on commit **only when the value actually changed**. Use this to update external state (controlled pattern).
- **`isEditing`** `boolean` — Controlled editing state. When provided, the editing state is controlled.
- **`defaultIsEditing`** `boolean` (default: `false`) — Default editing state for uncontrolled usage.
- **`onEditingChange`** `(isEditing: boolean) => void` — Called when editing mode starts or ends.
- **`onSubmit`** `(value: string) => void` — Fires every commit (Enter / submit-on-blur / `ref.stopEditing(true)`), even when the value did not change. Use for side effects (analytics, save toasts).
- **`onCancel`** `() => void` — Called when editing is cancelled (Escape or empty submit when `allowEmpty` is `false`).
- **`editTrigger`** `'dblclick' | 'click' | 'none'` (default: `dblclick`) — How edit mode is activated from the display element. Programmatic entry via `ref.startEditing()` works regardless.
- **`keyboardActivation`** `boolean` (default: `true`) — When `true`, the display element is keyboard-focusable (`tabIndex=0`, `role="button"`) and responds to `Enter`, `F2` and `Space` to enter edit mode. Set to `false` when the host already owns keyboard activation (e.g. an editable tab inside a `<button>`) — exposing the display as a separate tab stop would otherwise nest focus inside the host.
- **`submitOnBlur`** `boolean` (default: `true`) — Whether to submit when focus leaves the input.
- **`trimOnSubmit`** `boolean` (default: `true`) — Whether to trim the value on submit.
- **`allowEmpty`** `boolean` (default: `false`) — When `false`, submitting an empty/whitespace value cancels instead.
- **`isDisabled`** `boolean` (default: `false`) — When `true`, edit mode cannot be entered (programmatically or otherwise).
- **`isReadOnly`** `boolean` (default: `false`) — When `true`, edit mode cannot be entered, but the display reads as enabled.
- **`isStyled`** `boolean` (default: `false`) — When `true`, applies a styled wrapper (border, surface fill, padding, fixed height) so the component visually matches a `TextInput`. Useful when swapping a field component (e.g. `Select`) for an `InlineInput` to rename / edit the current value without a visual jump.
- **`placeholder`** `string` — Placeholder text shown in the input when the draft is empty.
- **`renderDisplay`** `(value: string) => ReactNode` — Custom render for display (non-editing) mode.
- **`tooltip`** `boolean | string | AutoTooltipValue` (default: `true`) — Tooltip behaviour for the display value. `true` enables auto-tooltip on overflow (showing the full text when truncated). A string always shows that text. `false` disables the tooltip. Suppressed automatically while editing and when `renderDisplay` is used.
- **`tooltipPlacement`** `'top' | 'bottom' | 'left' | 'right' | ...` (default: `'top'`) — Default tooltip placement.
- **`inputStyles`** `Styles` — Convenience prop for styling the `Input` sub-element.

### Base Properties

Supports [Base properties](../../BaseProperties.md).

### Imperative Ref

The component exposes a ref with these methods:

- **`startEditing()`** — Programmatically enter edit mode. Always works regardless of `editTrigger`; blocked only by `isDisabled` / `isReadOnly`.
- **`stopEditing(submit?: boolean)`** — Programmatically exit edit mode. `submit=true` (default) commits, `submit=false` cancels.
- **`focus()`** — Focus the underlying element (input when editing, root span otherwise).
- **`getValue()`** — Get the current committed value.

### Styling Properties

#### styles

Customizes the root element of the component. The root is `inline-flex` with `align-items: baseline` so it integrates cleanly into any inline text context (tabs, headings, paragraphs) without shifting surrounding text. Truncation lives on the inner `Display` sub-element so the root's baseline is not perturbed.

**Sub-elements:**

- `Display` — The block-level wrapper that holds the value in display mode and applies `overflow: hidden` / `text-overflow: ellipsis` for truncation
- `Input` — The text input rendered in edit mode
- `Placeholder` — The placeholder text rendered in display mode when the value is empty
- `Measure` — The hidden text used to auto-size the input (rarely targeted)

#### inputStyles

Convenience prop equivalent to `styles={{ Input: { ... } }}`.

### Style Properties

These properties allow direct style application without using the `styles` prop:

- **Position:** `gridArea`, `order`, `gridColumn`, `gridRow`, `placeSelf`, `alignSelf`, `justifySelf`, `zIndex`, `margin`, `inset`, `position`, `scrollMargin`
- **Dimension:** `width`, `height`, `flexBasis`, `flexGrow`, `flexShrink`, `flex`
- **Block:** `border`, `radius`, `shadow`, `outline`, `padding`, `paddingInline`, `paddingBlock`, `overflow`, `scrollbar`, `textAlign`
- **Color:** `color`, `fill`, `fade`, `image`

### Modifiers

The `mods` property is automatically populated. Consumers can target these in their own `styles`:

| Modifier   | Type      | Description                                                                       |
| ---------- | --------- | --------------------------------------------------------------------------------- |
| editing    | `boolean` | Currently in edit mode                                                            |
| editable   | `boolean` | `editTrigger` is `dblclick` or `click` and not disabled/read-only                 |
| focused    | `boolean` | The component is focused — keyboard-focused on the display, **or** while editing. Always `false` when `keyboardActivation={false}` (the host owns the focus indication). |
| disabled   | `boolean` | `isDisabled` is `true`                                                            |
| read-only  | `boolean` | `isReadOnly` is `true`                                                            |
| styled     | `boolean` | `isStyled` is `true`                                                              |
| empty      | `boolean` | The committed value is empty                                                      |

## Examples

### Basic Usage

```jsx
<InlineInput defaultValue="Edit me" />
```

### Single-click Trigger

Enter edit mode on a single click instead of double-click:

```jsx
<InlineInput defaultValue="Click me to edit" editTrigger="click" />
```

### Manual / Programmatic Trigger

When `editTrigger` is `none`, the display element is inert. Use the imperative ref to enter edit mode:

```jsx
const ref = useRef<CubeInlineInputRef>(null);

<>
  <InlineInput ref={ref} editTrigger="none" defaultValue="Use the button" />
  <Button onPress={() => ref.current?.startEditing()}>Rename</Button>
</>
```

### Controlled Value

Track the value externally:

```jsx
const [value, setValue] = useState('Controlled value');

<InlineInput value={value} onChange={setValue} />
```

### Controlled Editing State

Drive the editing state from outside (e.g. from a "Rename" button in a parent menu):

```jsx
const [editing, setEditing] = useState(false);

<InlineInput
  isEditing={editing}
  onEditingChange={setEditing}
  defaultValue="Controlled isEditing"
  editTrigger="none"
/>
```

### Inside a Heading

The component inherits all typography from its parent:

```jsx
<Title level={2}>
  <InlineInput defaultValue="Editable heading" />
</Title>
```

### Inline in Paragraph

Sits naturally inside text flow:

```jsx
<p>
  Hello, my name is <InlineInput defaultValue="John Doe" /> and I love editing
  text in place.
</p>
```

### Placeholder for Empty Values

```jsx
<InlineInput defaultValue="" placeholder="Untitled" />
```

### Allow Empty Values

By default, submitting an empty value cancels. Set `allowEmpty` to keep the empty value:

```jsx
<InlineInput defaultValue="Clearable" allowEmpty placeholder="Empty allowed" />
```

### Disabled / Read-only

```jsx
<InlineInput defaultValue="You can't edit me" isDisabled />
<InlineInput defaultValue="Read-only value" isReadOnly />
```

### Styled (TextInput Look)

Set `isStyled` to apply a styled wrapper (border, surface fill, padding, fixed height) that visually matches a `TextInput`. Use this when you need to swap a field component (e.g. `Select`) for an `InlineInput` to rename / edit the current value without a visual jump:

```jsx
<InlineInput isStyled width="200px" defaultValue="Click to rename" />
```

### Swap Select with Rename

A common pattern: a `Select` is replaced by a styled `InlineInput` when the user triggers a "Rename" action. The styled wrapper preserves the visual dimensions of the `Select` so the swap is seamless.

### Overflow & Tooltip

When the value is wider than its container, the display text is truncated with an ellipsis. The full value is shown as a tooltip on hover/focus — no extra props required.

```jsx
<div style={{ width: 200 }}>
  <InlineInput defaultValue="A very long inline value that gets truncated…" />
</div>
```

To always show a custom tooltip (regardless of overflow), pass a string. To disable the tooltip entirely, pass `false`:

```jsx
<InlineInput defaultValue="Hover me" tooltip="Click to rename" />
<InlineInput defaultValue="No tooltip" tooltip={false} />
```

The truncation behaviour is automatically disabled while editing so the input is never visually clipped while the user types.

### Keyboard Activation

By default the display element is keyboard-focusable (`tabIndex=0`, `role="button"`, `aria-roledescription="editable text"`). Pressing `Enter`, `F2` or `Space` enters edit mode without requiring a pointer. A keyboard focus ring (`#primary` token, rounded) is shown only when the element receives keyboard focus — clicks never trigger the ring.

When `InlineInput` is embedded in a host that already owns keyboard activation (e.g. `Tabs` — each tab is a `<button>` that listens for `F2` itself), pass `keyboardActivation={false}` to avoid creating a nested tab stop inside the host. The imperative `ref.startEditing()` always works regardless of this setting, and the focus ring is fully suppressed in this mode — **both in display and edit mode** — so the host's focus ring is the only one users see.

```jsx
<InlineInput defaultValue="Press Enter to edit" />

<InlineInput keyboardActivation={false} defaultValue="Host owns the keyboard" />
```

## Accessibility

### Keyboard Navigation

**Display mode** (when `keyboardActivation` is `true`):

- `Tab` — Moves focus to the value
- `Enter`, `F2`, `Space` — Enter edit mode

**Edit mode**:

- `Enter` — Commit the current draft
- `Escape` — Cancel and revert to the previous value
- `Tab` / focus loss — Commits when `submitOnBlur` is `true` (default), otherwise no-op
- Arrow keys, Space, Delete, Backspace — Work normally inside the input and do not bubble to the host

### ARIA Properties

- The display element receives `role="button"` and `aria-roledescription="editable text"` when keyboard-focusable, so screen readers announce that the value can be activated for editing.
- **`aria-label`** — Provide a label when the value alone doesn't convey purpose. Forwarded to both the display element (when focusable) and the inner input. Recommended in all cases.
- **`aria-labelledby`** — Reference the id of a visible label element. Forwarded the same way.
- **`aria-disabled`** / **`aria-readonly`** — Set automatically from `isDisabled` / `isReadOnly`.

### Focus Management

When entering edit mode, focus is taken explicitly by a `FocusScope` with `autoFocus`. This makes the transition deterministic even when triggered from a closing menu (which would otherwise restore focus to the menu trigger). The text content is selected automatically.

When leaving edit mode, focus is not restored automatically — the host is responsible for moving focus back to a meaningful element (e.g. the parent button or row).

## Best Practices

1. **Do**: Always provide an `aria-label` or `aria-labelledby` for screen reader users.

   ```jsx
   <InlineInput defaultValue="My document" aria-label="Document name" />
   ```

2. **Do**: Use `editTrigger="dblclick"` for items that are also clickable for selection (the default).

3. **Don't**: Use `InlineInput` as a form field. Use `TextInput` inside a `<Form>` for that.

4. **Don't**: Rely on `onChange` for keystroke-by-keystroke updates — it fires only on commit. Use `isEditing` + `onEditingChange` if you need to observe the editing lifecycle.

### onChange vs onSubmit

These look similar but fire under different conditions. Pick the one that matches your intent:

| Event       | Fires when                                                                | Typical use                              |
| ----------- | ------------------------------------------------------------------------- | ---------------------------------------- |
| `onChange`  | The committed value is **different** from the previous value             | Updating state (controlled pattern)      |
| `onSubmit`  | The user commits (Enter / blur / `ref.stopEditing(true)`) — always fires | Side effects (server save, toasts)       |
| `onCancel`  | Escape, empty submit (when `allowEmpty=false`), or `ref.stopEditing(false)` | Reverting UI, cleanup                    |

### Optimistic Display

When the component is **controlled** and the parent updates `value` asynchronously, the component shows the just-committed value optimistically until the parent's `value` catches up. This prevents a flicker back to the old value between commit and re-render.

### Async Save & Rollback

`onSubmit` may return a `Promise`. The component does not block the UI on the promise, but it watches the outcome:

- **Promise resolves** — the parent is expected to have updated `value` (or chose not to). Optimistic display clears when `value` next changes.
- **Promise rejects** — the component treats this as a save failure and **automatically reverts** the optimistic display back to the actual `value` prop. Stale optimistic state never lingers.

A typical async-save pattern:

```jsx
<InlineInput
  value={title}
  onSubmit={async (next) => {
    await saveToServer(next);  // throws on failure
    setTitle(next);            // only on success
  }}
/>
```

If `saveToServer` throws, `InlineInput` reverts to `title` automatically. If you want to show a toast on failure, wrap inside `onSubmit` and re-throw to keep the rollback:

```jsx
<InlineInput
  value={title}
  onSubmit={async (next) => {
    try {
      await saveToServer(next);
      setTitle(next);
    } catch (err) {
      showToast('Save failed');
      throw err;
    }
  }}
/>
```

If the user starts a new edit before a pending promise settles, the older promise's outcome is ignored (no late rollback can clobber the newer commit).

## Related Components

- [TextInput](../fields/TextInput.md) — Full form-aware text input
- [Tabs](../navigation/Tabs.md) — Uses `InlineInput` internally for editable tab titles
