Monet
@etched/monet 0.0.1

Inline edit

Ported

Text that turns into a field on click: one line, or several with multiline. Enter (Cmd+Enter for several lines) or leaving the field saves; Escape cancels. It takes the font of the text around it, so it works in a heading or a table cell. Native <span> attributes and ref pass through to the outer element.

import { useState } from "react";
import { Text } from "@etched/monet/text";
import { InlineEdit } from "@etched/monet/inline-edit";

export function Preview() {
  const [name, setName] = useState("Ana Ruiz");

  return (
    <Text as="h3" variant="heading" size="lg">
      <InlineEdit aria-label="Name" value={name} onSave={setName} />
    </Text>
  );
}

Installation

import { InlineEdit } from "@etched/monet/inline-edit";

Examples

Name, title and company

Click the text, or press Enter on it. Enter or leaving the field saves; Escape cancels.

at

import { useState } from "react";
import { Text } from "@etched/monet/text";
import { InlineEdit } from "@etched/monet/inline-edit";

export function NameTitleAndCompany() {
  const [person, setPerson] = useState({ name: "José Núñez", title: "", company: "Tenstorrent" });

  return (
    <div className="grid">
      <Text as="h3" variant="heading" size="lg">
        <InlineEdit aria-label="Name" value={person.name} onSave={(name) => setPerson({ ...person, name })} />
      </Text>
      <Text as="p">
        <InlineEdit aria-label="Title" placeholder="Add a title" value={person.title} onSave={(title) => setPerson({ ...person, title })} />
        {" at "}
        <InlineEdit aria-label="Company" value={person.company} onSave={(company) => setPerson({ ...person, company })} />
      </Text>
    </div>
  );
}

Notes

multiline keeps line breaks, and the field grows with the text. Enter starts a new line; Cmd+Enter or Ctrl+Enter saves.

import { useState } from "react";
import { InlineEdit } from "@etched/monet/inline-edit";

export function Notes() {
  const [notes, setNotes] = useState("Strong on clock trees.\nWants to stay close to silicon bring-up; ask about the Taipei move.");

  return (
    <div className="grid">
      <InlineEdit aria-label="Notes" placeholder="Add notes" multiline value={notes} onSave={setNotes} />
    </div>
  );
}

Errors and slow saves

Throw from onSave to show a message: the field stays open with the typed text. A slow save shows the field as busy.

import { useState } from "react";
import { InlineEdit } from "@etched/monet/inline-edit";

export function ErrorsAndSlowSaves() {
  const [name, setName] = useState("Lee Park");

  const save = async (next: string) => {
    if (!next.trim()) throw new Error("Enter a name.");
    await new Promise((done) => setTimeout(done, 600));
    setName(next.trim());
  };

  return <InlineEdit aria-label="Name" value={name} onSave={save} />;
}

Opened from outside

editing and onEditingChange let the app open the field, for example when Tab moves to the next field. Keys other than Enter and Escape bubble up from the field.

import { useState } from "react";
import { Button } from "@etched/monet/button";
import { InlineEdit } from "@etched/monet/inline-edit";

export function OpenedFromOutside() {
  const [title, setTitle] = useState("Thermal engineer");
  const [editing, setEditing] = useState(false);

  return (
    <div className="row">
      <InlineEdit aria-label="Title" value={title} onSave={setTitle} editing={editing} onEditingChange={setEditing} />
      <Button size="sm" onClick={() => setEditing(true)}>
        Edit title
      </Button>
    </div>
  );
}

In a table

In a one-line table (Table truncate), long text is cut with an ellipsis; the field shows all of it.

Ana Ruiz
import { useState } from "react";
import { Table } from "@etched/monet/table";
import { InlineEdit } from "@etched/monet/inline-edit";

export function InATable() {
  const [title, setTitle] = useState("Distributed Systems Engineer, Infrastructure and Platform Reliability");

  return (
    <Table aria-label="People" truncate style={{ inlineSize: "24rem", tableLayout: "fixed" }}>
      <tbody>
        <tr>
          <td>Ana Ruiz</td>
          <td>
            <InlineEdit aria-label="Long title" value={title} onSave={setTitle} />
          </td>
        </tr>
      </tbody>
    </Table>
  );
}

Accessibility

  • Enter or Space on the text opens the field with its text selected. Enter saves (Cmd+Enter or Ctrl+Enter with multiline); Escape cancels.
  • After Enter or Escape, focus goes back to the text. Leaving the field, with Tab or a click, also saves.
  • At rest it is a button named "Edit", the aria-label and the value, such as "Edit Company: Etched", or by editLabel. Open, the field is named by aria-label.
  • A save error has role="alert" and describes the field, which gets aria-invalid. During a save, the field is read-only with aria-busy.
  • Give aria-label a short field name, such as "Company". To refuse a value, throw an Error with a message from onSave.

API reference

PropTypeDefaultDescription
value *string–
onSave *(value: string) => void | Promise<void>–Saves the new text. Throw (or reject) with a message to show it as an error; the field then stays open. Runs only when the text changed.
aria-label *string–Names the field, for example "Name". The text at rest is named "Edit Name: …", so it is clear that it opens a field.
placeholderstring–Shown at rest while the value is empty.
editLabel(value: string) => string`Edit ${aria-label}: ${value}` (or the placeholder, or "empty")Names the text at rest, the button that opens the field. Gets the value.
multilineboolean–Several lines, for notes: the text keeps its line breaks, and the field grows with it. Enter starts a new line; Cmd+Enter or Ctrl+Enter saves.
disabledboolean–The text cannot be opened, and an open field cannot change or save.
editingboolean–Whether the field is open, to open it from outside (for example when Tab moves to the next field). Leave it out to let a click open it.
onEditingChange(editing: boolean) => void–Called when the field opens or closes.
classNamestring–Class on the outer element, for layout.