# Typeahead

## Usage

`Typeahead` is a single-select search field. The input is a combobox and the menu is a listbox. Use `createStaticSource` for local data, or implement `search` for a remote source.

Local search matches the label and optional keywords. Set `debounceMs` to `0` to filter immediately:

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { Typeahead } from '@astryx-vue/core/Typeahead'
import { ref } from 'vue'

const selected = ref(null)
const source = createStaticSource([
  { id: 'apple', label: 'Apple' },
  { id: 'grape', label: 'Grape' },
], { keywords: item => item.id === 'grape' ? ['raisin'] : [] })
</script>

<template>
  <Typeahead v-model="selected" :search-source="source" label="Fruit" :debounce-ms="0" show-on-focus />
</template>
```

An async source returns a Promise. `useAsyncSource` waits until typing pauses, 150 milliseconds by default, then starts the request. A newer search cancels the previous one, and a stale response is dropped. An empty query uses `bootstrap` when it exists. Loading, no results, and a failed request each get their own row in the menu.

Arrow keys move the highlight, `Enter` selects it, and `Escape` closes the menu. While focus is in the input, `Home` and `End` move the caret. After a selection, the value collapses to a focusable token. Focusing it again returns to search.

Built-in strings are English. Call `provideAstryxLocale('zh-CN')` for the Chinese strings.

## Best Practices

| Guideline | Practice |
| --- | --- |
| Do | Set `debounceMs` to 0 for local data, and keep the default delay for remote requests. |
| Do | Abort the request when `signal` is cancelled so an older response cannot replace a newer one. |
| Don't | Use typeahead for multiple values. Use Multi Selector for a fixed list, and Tokenizer for searchable tags. |

### Anatomy

- **Field**: Standalone Field shell that provides the label and optional supporting content; omitted inside InputGroup.
- **Input surface**: Painted control surface containing the editable input or the selected token.
- **Icon-rendered start icon**: Optional leading semantic icon or icon component rendered through Icon.
- **Caller-rendered start content**: Optional arbitrary slot content rendered directly at the start of the input surface.
- **Selected token**: Token that presents the selected item while the control is not in edit mode.
- **Spinner**: Loading indicator shown at the end of the input surface while a search is in flight.
- **Clear button**: Shared clear action that removes the selected item when clearable is set.
- **Dropdown**: Anchored listbox surface containing the current search results.
- **Empty state**: Message shown after a completed search returns no results.
- **Result row**: Stable option wrapper that owns highlight, selection, pointer, and keyboard behavior.
- **Default item content**: The standard label and supporting content rendered inside a result row when the item slot is not provided.
- **Caller-rendered item content**: Caller-owned result content supplied through the item slot, inside the stable result row.
- **Result group heading**: Visible heading for a labeled group of result rows.
- **Selected result state**: Selected styling and trailing check presented on the current result row.

## Examples

### Limited Results

Typeahead with a capped dropdown showing at most three results.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { Typeahead } from '@astryx-vue/core/Typeahead'
import { computed, ref } from 'vue'

const selected = ref(null)
const source = computed(() => createStaticSource([
  { id: 'us', label: 'United States' },
  { id: 'uk', label: 'United Kingdom' },
  { id: 'ca', label: 'Canada' },
  { id: 'au', label: 'Australia' },
  { id: 'de', label: 'Germany' },
  { id: 'fr', label: 'France' },
  { id: 'jp', label: 'Japan' },
  { id: 'br', label: 'Brazil' },
]))
</script>

<template>
  <div class="demo-container">
    <Typeahead
      v-model="selected"
      :search-source="source"
      label="Country"
      placeholder="Search countries..."
      :debounce-ms="0"
      :max-menu-items="3"
      has-entries-on-focus
    />
  </div>
</template>

<style scoped>
.demo-container {
  width: 20rem;
  max-width: calc(100vw - 4rem);
}
</style>
```

### Search Field

Search input with icon and suggestions on focus.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { Typeahead } from '@astryx-vue/core/Typeahead'
import { computed, ref } from 'vue'

const selected = ref(null)
const source = computed(() => createStaticSource([
  { id: 'olivia', label: 'Olivia Martin' },
  { id: 'jackson', label: 'Jackson Lee' },
  { id: 'isabella', label: 'Isabella Nguyen' },
  { id: 'william', label: 'William Kim' },
  { id: 'sofia', label: 'Sofia Davis' },
]))
</script>

<template>
  <div class="demo-container">
    <Typeahead
      v-model="selected"
      :search-source="source"
      label="Team member"
      placeholder="Search people..."
      :debounce-ms="0"
      has-entries-on-focus
    >
      <template #icon>
        <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="7" cy="7" r="4.5" /><path d="m10.5 10.5 3 3" /></svg>
      </template>
    </Typeahead>
  </div>
</template>

<style scoped>
.demo-container {
  width: 20rem;
  max-width: calc(100vw - 4rem);
}
.demo-container :deep(svg) {
  width: 1rem;
  height: 1rem;
}
</style>
```

### With Helper Text

Typeahead with a description below the label.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { Typeahead } from '@astryx-vue/core/Typeahead'
import { computed, ref } from 'vue'

const selected = ref(null)
const source = computed(() => createStaticSource([
  { id: 'eng', label: 'Engineering' },
  { id: 'design', label: 'Design' },
  { id: 'marketing', label: 'Marketing' },
  { id: 'sales', label: 'Sales' },
  { id: 'product', label: 'Product' },
]))
</script>

<template>
  <div class="demo-container">
    <Typeahead
      v-model="selected"
      :search-source="source"
      label="Department"
      placeholder="Search departments..."
      description="Select the department this request should be routed to"
      :debounce-ms="0"
    />
  </div>
</template>

<style scoped>
.demo-container {
  width: 20rem;
  max-width: calc(100vw - 4rem);
}
</style>
```

### With Validation

Typeahead with an error validation message.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { Typeahead } from '@astryx-vue/core/Typeahead'
import { computed, ref } from 'vue'

const selected = ref(null)
const source = computed(() => createStaticSource([
  { id: 'ny', label: 'New York' },
  { id: 'sf', label: 'San Francisco' },
  { id: 'london', label: 'London' },
  { id: 'berlin', label: 'Berlin' },
  { id: 'tokyo', label: 'Tokyo' },
]))
</script>

<template>
  <div class="demo-container">
    <Typeahead
      v-model="selected"
      :search-source="source"
      label="Office"
      placeholder="Search offices..."
      status="error"
      status-message="Please select an office location"
      :debounce-ms="0"
    />
  </div>
</template>

<style scoped>
.demo-container {
  width: 20rem;
  max-width: calc(100vw - 4rem);
}
</style>
```

### Without field chrome

BaseTypeahead has no field label or description. Use it where the page already names the control, such as a command palette, and pass label as the input's accessible name.

```vue
<script setup lang="ts">
import { createStaticSource } from '@astryx-vue/core'
import { BaseTypeahead } from '@astryx-vue/core/Typeahead'
import { computed, ref } from 'vue'

const selected = ref(null)
const source = computed(() => createStaticSource([
  { id: 'files', label: 'Search files', description: 'Jump to a file' },
  { id: 'settings', label: 'Open settings', description: 'Preferences' },
  { id: 'theme', label: 'Toggle theme', description: 'Light or dark' },
]))
</script>

<template>
  <div class="demo-container">
    <BaseTypeahead
      v-model="selected"
      :search-source="source"
      label="Command"
      placeholder="Type a command"
      :debounce-ms="0"
      has-entries-on-focus
    />
  </div>
</template>

<style scoped>
.demo-container {
  width: 20rem;
  max-width: calc(100vw - 4rem);
}
</style>
```

## Typeahead Props

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `T \| null` | `null` | Selected item. An item has at least `id` and `label`. |
| `searchSource` | `SearchSource` | Required | Static or async source. An empty query prefers `bootstrap`. |
| `label` | `string` | Required | Field label. Always available to assistive technology. |
| `description` | `string` | — | Helper text under the label. |
| `labelTooltip` | `string` | — | Tooltip text on an info icon next to the label. |
| `isLabelHidden` | `boolean` | `false` | Hides the label visually while keeping it accessible. `labelHidden` is the older name; either one hides it. |
| `containerClass` / `controlClass` | `class` | — | Field shell and input box. A root class lands on the box, not the native input. An item `action` turns the menu into two columns and does not select the item. |
| `startIcon` | `Component` | — | Icon at the start of the input. An `icon` slot replaces it. |
| `placeholder` | `string` | `Search…` / `搜索…` | Placeholder. English unless a locale is provided. |
| `debounceMs` | `number` | `150` | Wait after typing before searching. Use 0 for local data. |
| `minQueryLength` | `number` | `1` | Shortest query that gets searched, counted in visible characters. Below it nothing is searched and the menu stays closed, so a query nobody searched never reports "no results". An empty query is not "too short": it still bootstraps. |
| `hasEntriesOnFocus` | `boolean` | `false` | Show bootstrap results on focus before the user types. |
| `showOnFocus` | `boolean` | `false` | Legacy name for `hasEntriesOnFocus`. Either one turns it on. |
| `hasAutoFocus` | `boolean` | `false` | Focus the input on mount. |
| `maxMenuItems` | `number` | `10` | Maximum rows in the menu. |
| `hasClear` | `boolean` | `true` | Show a clear button that deselects the current value. |
| `clearable` | `boolean` | `true` | Legacy name for `hasClear`. Turning either one off removes the button. |
| `emptySearchText` | `string \| null` | `'No results found'` / `'未找到结果'` | Text shown when the query matched nothing. `null` means "not given" and falls through to the default; an empty string renders nothing. For rich content use the `empty` slot. |
| `emptyText` | `string \| null` | — | Legacy name for `emptySearchText`. |
| `disabled` | `boolean` | `false` | Disable the field. |
| `disabledMessage` | `string` | — | Explains why the field is disabled. With `disabled`, the control stays focusable through `aria-disabled` and `readonly` instead of the native `disabled`, and the message shows as a tooltip on hover or keyboard focus. |
| `disabledReason` | `string` | — | Legacy name for `disabledMessage`. |
| `status` | `'default' \| 'success' \| 'warning' \| 'error'` | `'default'` | Status style. |
| `statusMessage` | `string` | — | Status text under the field. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input and token size. |
| `required` | `boolean` | `false` | Marks the field as required. |
| `optional` | `boolean` | `false` | Shows an optional indicator on the label. |
| `name` | `string` | — | Form field name. Submits the selected id. |
| `width` | `number \| string` | — | Width of the whole field (number = pixels, string used as-is). |

## Typeahead Emits

| Name | Parameters | Description |
| --- | --- | --- |
| `update:modelValue` | `value: T \| null` | Emitted when the selection is set or cleared. |
| `change` | `value: T \| null` | Emitted when the selection is set or cleared. |
| `changeQuery` | `query: string` | Emitted when the query text changes. |
| `search` | `query: string` | Legacy name for `changeQuery`. Both fire. |
| `openChange` | `isOpen: boolean` | Emitted when the menu opens or closes. |

## Typeahead Slots

| Name | Parameters | Description |
| --- | --- | --- |
| `icon` | — | Icon at the start of the input. |
| `item` | `{ item, isActive, isSelected }` | Custom menu row. Replaces the default `TypeaheadItem`. |
| `empty` | — | Custom empty state. |

## createStaticSource

| Name | Type | Description |
| --- | --- | --- |
| `items` | `T[]` | Local items. |
| `keywords` | `(item: T) => string[]` | Extra words included in the substring match. |

## Grouping results

Put a group name on `item.auxiliaryData.group`. Rows that share a name are rendered as one `role="group"` block with a visible heading, in the order the groups first appear. Rows without a group stay at their own position.

```ts
const items = [
  { id: 'apple', label: 'Apple', auxiliaryData: { group: 'Fruit' } },
  { id: 'carrot', label: 'Carrot', auxiliaryData: { group: 'Vegetable' } },
]
```

## BaseTypeahead

No field label, description, or validation message. Use it where the surrounding surface already has a title, such as a command palette. `label` here is the input's accessible name; omit it when an outside `<label for>` already names the input. The default menu row is `TypeaheadItem`, and the `item` slot replaces it.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `T \| null` | `null` | Selected item. |
| `searchSource` | `SearchSource` | Required | Static or async source. |
| `label` | `string` | — | Accessible name of the input. |
| `inputId` | `string` | Generated | Id of the input, so an outside label can point at it. |
| `describedBy` | `string` | — | Ids of the description and status the caller already computed. |
| `ariaDescribedBy` | `string` | — | Input-specific alias for `aria-describedby`. Takes precedence over the native attribute. |
| `ariaLabelledBy` | `string` | — | Input-specific alias for `aria-labelledby`. Takes precedence over the native attribute. |
| `anchorRef` | `HTMLElement \| null` | — | Element the dropdown hangs from and measures itself against. Defaults to the input's own wrapper. |
| `inputTabIndex` | `number` | — | Tab order of the input. Defaults to `-1` while the selected token covers the input, so the invisible input is not a tab stop. |
| `isFocusableDisabled` | `boolean` | `false` | Keep a disabled input focusable through `aria-disabled` and `readonly` so a caller-owned disabled reason stays discoverable. Text entry stays blocked. |
| `hasAutoFocus` | `boolean` | `false` | Focus the input on mount. |
| `hasEntriesOnFocus` | `boolean` | `false` | Show bootstrap results on focus before the user types. |
| `showOnFocus` | `boolean` | `false` | Legacy name for `hasEntriesOnFocus`. Either one turns it on. |
| `minQueryLength` | `number` | `1` | Shortest query that gets searched, counted in visible characters. Below it nothing is searched and the menu stays closed. |
| `menuWidth` | `number` | — | Requested dropdown width in pixels. Never narrower than the anchor, and clamped to the viewport. |
| `maxMenuItems` | `number` | `10` | Maximum rows in the menu. |
| `emptySearchText` | `string \| null` | `'No results found'` / `'未找到结果'` | Text shown when the query matched nothing. `null` falls through to the default; an empty string renders nothing. |
| `emptyText` | `string \| null` | — | Legacy name for `emptySearchText`. |
| `placeholder` | `string` | `Search…` | Input placeholder. |
| `disabled` | `boolean` | `false` | Disable the input. |
| `disabledReason` | `string` | — | Why the input is disabled. Shown as a tooltip and announced as the description while the input stays focusable. |
| `status` | `'default' \| 'success' \| 'warning' \| 'error'` | `'default'` | Border color of the control. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input and token size. |
| `clearable` | `boolean` | `true` | Show a clear button. |
| `debounceMs` | `number` | `150` | Wait after typing before searching. Use 0 for local data. |
| `required` | `boolean` | `false` | Marks the input as required. |
| `name` | `string` | — | Form field name. Submits the selected id. |

| Name | Parameters | Description |
| --- | --- | --- |
| `update:modelValue` | `value: T \| null` | Emitted when the selection is set or cleared. |
| `change` | `value: T \| null` | Emitted when the selection is set or cleared. |
| `changeQuery` | `query: string` | Emitted when the query text changes. |
| `search` | `query: string` | Legacy name for `changeQuery`. Both fire. |
| `openChange` | `isOpen: boolean` | Emitted when the menu opens or closes. |

Native attributes and listeners that are not listed above land on the input element, so `@keydown`, `@focus`, `@blur` and `aria-*` work as they would on a plain `<input>`. A `keydown` listener runs before the internal keyboard navigation; call `event.preventDefault()` to skip it.

## TypeaheadItem

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `item` | `SearchableItem` | Required | The search result this row shows. |
| `description` | `string` | — | Overrides the item's own description. |
| `group` | `string` | — | Accepted for API parity; the row itself draws no heading. Grouping is done by the menu from `item.auxiliaryData.group`. |
| `isDisabled` | `boolean` | `false` | Paints the row as unavailable. |
| `icon` (slot) | — | — | Icon or avatar shown before the label. |
