# Example: Use your own editor controls

Native input, textarea, and icon-button replacements preserve built-in editing, including a number stepper, multiline drafts, and delayed saves with a corrected retry.

Source: https://pretable.ai/examples/components-editors.md

```tsx EditorControlsGrid.tsx
"use client";

import { useState } from "react";
import { PretableSurface, type PretableColumn } from "@pretable/react";

import { AppIconButton, AppTextarea, AppTextInput } from "./EditorControls";
import "./editor-controls.css";

interface Item {
  id: string;
  name: string;
  quantity: number | null;
  notes: string;
}

const initialRows: Item[] = [
  {
    id: "notebook",
    name: "Notebook",
    quantity: 4,
    notes: "Dot grid\nRecycled paper",
  },
  { id: "pencil", name: "Pencil", quantity: 12, notes: "Pack of twelve" },
  { id: "folder", name: "Folder", quantity: 6, notes: "For project notes" },
];

const columns: PretableColumn<Item>[] = [
  { id: "name", header: "Name", editable: true, widthPx: 170 },
  {
    id: "quantity",
    header: "Quantity",
    type: "number",
    editable: true,
    widthPx: 150,
  },
  { id: "notes", header: "Notes", editable: true, wrap: true, widthPx: 270 },
];

export function EditorControlsGrid() {
  const [rows, setRows] = useState(initialRows);
  const [status, setStatus] = useState(
    "Ready to edit. Saves take about 800ms.",
  );

  return (
    <div>
      <p style={{ margin: "0 0 12px", fontSize: 13 }}>
        Double-click a cell or select it and press <kbd>F2</kbd>. Try the
        Quantity stepper, then enter <strong>-1</strong> and press{" "}
        <kbd>Enter</kbd> to see a rejected save. Correct the value and press{" "}
        <kbd>Enter</kbd> again. In Notes, <kbd>Enter</kbd> adds a line;{" "}
        <kbd>Ctrl</kbd> / <kbd>Cmd</kbd> + <kbd>Enter</kbd> saves.{" "}
        <kbd>Escape</kbd> cancels.
      </p>
      <PretableSurface
        ariaLabel="Inventory with custom editors"
        rows={rows}
        columns={columns}
        getRowId={(row) => row.id}
        viewportHeight={230}
        components={{
          TextInput: AppTextInput,
          Textarea: AppTextarea,
          IconButton: AppIconButton,
        }}
        onRowChange={async ({ rowId, columnId, value, row }) => {
          setStatus(`Saving ${columnId}…`);
          await new Promise<void>((resolve) => setTimeout(resolve, 800));
          if (
            columnId === "quantity" &&
            typeof value === "number" &&
            value < 0
          ) {
            setStatus(
              "Quantity rejected. Correct it and press Enter to retry.",
            );
            throw new Error("Quantity must be zero or greater");
          }
          setRows((current) =>
            current.map((item) => (item.id === rowId ? row : item)),
          );
          setStatus(`Saved ${columnId}.`);
        }}
      />
      <p role="status" style={{ margin: "8px 0 0", fontSize: 13 }}>
        {status}
      </p>
    </div>
  );
}
```

```tsx EditorControls.tsx
import { forwardRef } from "react";
import type {
  PretableIconButtonProps,
  PretableTextareaProps,
  PretableTextInputProps,
} from "@pretable/react";

// Keep these component identities stable: defining them inside the grid
// would remount the active editor each time the parent renders.
export const AppTextInput = forwardRef<
  HTMLInputElement,
  PretableTextInputProps
>(function AppTextInput({ site, className, ...props }, ref) {
  return (
    <input
      {...props}
      ref={ref}
      className={["app-editor-input", className].filter(Boolean).join(" ")}
      data-pretable-text-input=""
      data-pretable-site={site}
    />
  );
});

export const AppTextarea = forwardRef<
  HTMLTextAreaElement,
  PretableTextareaProps
>(function AppTextarea({ site, className, ...props }, ref) {
  return (
    <textarea
      {...props}
      ref={ref}
      className={["app-editor-textarea", className].filter(Boolean).join(" ")}
      data-pretable-textarea=""
      data-pretable-site={site}
    />
  );
});

export const AppIconButton = forwardRef<
  HTMLButtonElement,
  PretableIconButtonProps
>(function AppIconButton({ site, className, ...props }, ref) {
  return (
    <button
      {...props}
      ref={ref}
      type="button"
      className={["app-editor-icon-button", className]
        .filter(Boolean)
        .join(" ")}
      data-pretable-icon-button=""
      data-pretable-site={site}
    />
  );
});

// The spread preserves value, native handlers (including composition),
// disabled, ARIA attributes, data attributes, children, and inline styles.
// The kit attributes above keep @pretable/ui's layout and state styling.
```

```css editor-controls.css
/* Keep the skin's editor sizing, focus, saving, and invalid states. */
.app-editor-input,
.app-editor-textarea {
  border-radius: 6px;
  font-family: inherit;
  background: color-mix(in srgb, var(--pretable-bg-grid) 94%, #0d9488);
}

.app-editor-icon-button {
  border-radius: 4px;
  color: var(--pretable-accent);
}

.app-editor-icon-button:not(:disabled):hover {
  background: color-mix(in srgb, var(--pretable-bg-grid) 85%, #0d9488);
}
```
