# Autocomplete Field

A labeled autocomplete input with a filtered suggestion popup and helper or error text.

## Installation

```tsx
import { AutocompleteField } from "@makeplane/propel/components/autocomplete-field";
```
## Usage

```tsx
import { AutocompleteField } from "@makeplane/propel/components/autocomplete-field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronExpandOutline, CloseOutline } from "@makeplane/propel/icons";

const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];

export default function BasicDemo() {
  return (
    <AutocompleteField
      name="containerImage"
      label="Container image"
      description="Type or choose an image tag."
      size="lg"
      items={IMAGES}
      placeholder="e.g. node:22-slim"
      empty="No images found"
      clear={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Clear container image"
          icon={<Icon icon={CloseOutline} />}
        />
      }
      trigger={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Open container image"
          icon={<Icon icon={ChevronExpandOutline} />}
        />
      }
    />
  );
}
```

### Result identity and limits

Primitive string or number options key as themselves — no `getItemKey` needed. Record options
**require** `getItemKey={(item) => item.id}` (or another stable unique identity); TypeScript enforces
this. Keys must remain stable when results reorder; display labels may repeat.

The field displays up to 50 matches by default. When the shown list hits that cap and the source
list is longer, a polite status region announces `truncatedMessage` (default: “Keep typing to narrow
results”; overridable / suppressible with `null`). The hint is possibly-truncated — it does not
invent an exact remainder — so custom `filter` functions stay correct without a second full-catalog
pass. The searchable catalog is never truncated. `limit={-1}` displays every match and should be
reserved for bounded lists. `limit={0}` renders no rows, but the popup still opens and shows
`empty` even when matches exist — it is not a way to hold suggestions back. The same limit applies
to server-ranked `filteredItems`. Fetching stays application-owned; use `value` and `onValueChange`
for the query and pass `[]` for an empty response. For browsing large, accumulated result lists, use
[Combobox Field](/components/combobox-field).

## Examples

### Sizes

`size` steps the label, description, and helper text together with the input group and its input text.

```tsx
import { AutocompleteField, type FieldSize } from "@makeplane/propel/components/autocomplete-field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton, type IconButtonSize } from "@makeplane/propel/components/icon-button";
import { ChevronExpandOutline, CloseOutline } from "@makeplane/propel/icons";

const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];

const SIZES: FieldSize[] = ["lg", "xl", "2xl"];

// The icon button rung inside each field's row rung: one ladder rung down, clamped at `lg` (the
// compact scale's ceiling) once the field size outgrows it.
const ICON_BUTTON_SIZE: Record<FieldSize, IconButtonSize> = {
  md: "sm",
  lg: "md",
  xl: "lg",
  "2xl": "lg",
};

export default function SizesDemo() {
  return (
    <div className="flex flex-col gap-4">
      {SIZES.map((size) => (
        <AutocompleteField
          key={size}
          name="containerImage"
          label={size}
          description="Type or choose an image tag."
          size={size}
          items={IMAGES}
          placeholder="e.g. node:22-slim"
          empty="No images found"
          clear={
            <IconButton
              variant="ghost"
              size={ICON_BUTTON_SIZE[size]}
              aria-label="Clear container image"
              icon={<Icon icon={CloseOutline} />}
            />
          }
          trigger={
            <IconButton
              variant="ghost"
              size={ICON_BUTTON_SIZE[size]}
              aria-label="Open container image"
              icon={<Icon icon={ChevronExpandOutline} />}
            />
          }
        />
      ))}
    </div>
  );
}
```

### Invalid

Setting `error` marks the field invalid, recolors the input group border, and replaces the description with the error text.

```tsx
import { AutocompleteField } from "@makeplane/propel/components/autocomplete-field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronExpandOutline, CloseOutline } from "@makeplane/propel/icons";

const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];

export default function InvalidDemo() {
  return (
    <AutocompleteField
      name="containerImage"
      label="Container image"
      size="lg"
      items={IMAGES}
      placeholder="e.g. node:22-slim"
      empty="No images found"
      error="Enter a container image."
      clear={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Clear container image"
          icon={<Icon icon={CloseOutline} />}
        />
      }
      trigger={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Open container image"
          icon={<Icon icon={ChevronExpandOutline} />}
        />
      }
    />
  );
}
```

### Disabled

```tsx
import { AutocompleteField } from "@makeplane/propel/components/autocomplete-field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronExpandOutline, CloseOutline } from "@makeplane/propel/icons";

const IMAGES = ["nginx:1.29-alpine", "node:22-slim", "postgres:18", "redis:8.2.2-alpine"];

export default function DisabledDemo() {
  return (
    <AutocompleteField
      name="containerImage"
      label="Container image"
      description="Type or choose an image tag."
      size="lg"
      disabled
      items={IMAGES}
      placeholder="e.g. node:22-slim"
      empty="No images found"
      clear={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Clear container image"
          icon={<Icon icon={CloseOutline} />}
        />
      }
      trigger={
        <IconButton
          variant="ghost"
          size="md"
          aria-label="Open container image"
          icon={<Icon icon={ChevronExpandOutline} />}
        />
      }
    />
  );
}
```
## API Reference

### AutocompleteField

Ready-to-use autocomplete field with label, input, popup items, and helper/error text.

Record options require `getItemKey`. Primitive string/number options key as themselves when it is
omitted.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `clear` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The clear control (e.g. an `IconButton`) carrying its own localizable `aria-label`. Required here (unlike the bare `AutocompleteInputGroup`) — a labeled field is always presented as a complete picker, not a plain search box. |
| `trigger` (required) | `ReactElement<unknown, string \| JSXElementConstructor<any>>` | — | The popup-trigger control (e.g. an `IconButton`) carrying its own localizable `aria-label`. Required here for the same reason as `clear`. |
| `empty` (required) | `ReactNode` | — | Message rendered when no item matches. |
| `items` (required) | `readonly Value[]` | — | Searchable catalog in local mode, or loaded results with `filteredItems` in external mode. Only the first `limit` matches render. The limit bounds rendering, not full-catalog filtering; use external search for huge catalogs. Use ComboboxField to scroll accumulated results. |
| `label` (required) | `string` | — | Visible field label. |
| `size` (required) | `"md" \| "lg" \| "xl" \| "2xl"` | — | Label and helper text size. |
| `getItemKey` (required) | `(item: Value) => Key` | — | Stable, unique identity for each result, independent of its display label or current index. Required for record options: labels may repeat, and object identity is not enough when a server replaces or reorders results. Primitive string/number options key as themselves. |
| `limit` | `number` | `50` | The maximum number of items to display in the list. Maximum displayed matches for local and external filtering. Use `-1` only for a bounded list that must display every match. `0` renders no rows, but the popup still opens and shows `empty` even when matches exist — it is not a “hold suggestions back” mode. This never truncates the source catalog. |
| `description` | `ReactNode` | — | Supporting text shown below the input. Replaced by `error` when an error is set. |
| `error` | `ReactNode` | — | Error text shown below the control. |
| `hint` | `ReactNode` | — | Helper text shown below the control. Replaced by `error` when an error is set. |
| `filteredItems` | `readonly Value[]` | — | Externally filtered or server-ranked matches. The same result limit applies to this list. |
| `truncatedMessage` | `ReactNode` | `"Showing the first {limit} matches. Keep typing to narrow results."` | Announced in the popup when the result cap may be hiding matches (shown count equals `limit` and the source list is longer). Override to localize; pass `null` to suppress the hint. |
| `placeholder` | `string` | — | Input placeholder. |
