# Base Typeahead

## Usage

Composable combobox engine providing a bare input, search, keyboard navigation, and a styled result dropdown. It renders no input wrapper, border, or selected-value token. `Typeahead` and `Tokenizer` compose it for standard fields.

```ts
import { BaseTypeahead } from '@astryx-vue/core/Typeahead'
```

The port keeps the same division of work. `BaseTypeahead` owns the input, the search source, the keyboard model, and the dropdown. The caller owns the field label, the description, the status message, and the selected-value token, and puts them around the engine. `label` here is only the input's accessible name, so omit it when an outside `<label for>` already names the input.

### Anatomy

- **Input**: Bare combobox input. The caller supplies its visible field chrome and accessible name.
- **Loading status**: Named Spinner shown beside the input while an asynchronous source is pending, unless a composed owner takes over the busy indicator lane.
- **Dropdown**: Anchored listbox surface containing current search or bootstrap results.
- **Empty state**: Disabled listbox option shown after a completed search returns no results.
- **Result group heading**: Visible label for a group of result options.
- **Result row**: Option wrapper that owns highlight, selection, pointer, and keyboard behavior.
- **Default item content**: TypeaheadItem label and optional supporting content rendered inside a result row.
- **Caller-rendered item content**: Caller content supplied through renderItem or item.element inside the stable result row.
- **Selected result state**: Selected row weight and trailing check shown when a result matches value.

## Best practices

| Guidance | Practice |
| --- | --- |
| Do | Use `Typeahead` or `Tokenizer` for standard fields; they wrap `BaseTypeahead` with input chrome and selected-value rendering it intentionally omits. |
| Do | Provide your own visible label or `aria-label` and custom input wrapper so the bare combobox has an accessible name, focus treatment, border, and layout. |
| Do | Pass `anchorRef` pointing to your wrapper so the dropdown positions against your custom input chrome, not just the bare input element. |
| Avoid | Expect input chrome or selected-value rendering. `BaseTypeahead` is an engine; the caller owns those visible parts. |
| Avoid | Use `BaseTypeahead` when `Typeahead` or `Tokenizer` would suffice; the extra wrapper and styling work is only justified for truly custom compositions. |
| Avoid | Treat Escape as cancellation of pending source work. It hides the current popup, but a late response can reopen it. |

## Examples

Common configurations, variations, and states.

### BaseTypeahead

A custom result renderer that adds supporting metadata while BaseTypeahead retains option semantics and keyboard behavior.

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

interface FrameworkItem {
  id: string
  label: string
  auxiliaryData: { category: string }
}

const frameworks: FrameworkItem[] = [
  { id: 'react', label: 'React', auxiliaryData: { category: 'UI library' } },
  { id: 'remix', label: 'Remix', auxiliaryData: { category: 'Web framework' } },
  { id: 'next', label: 'Next.js', auxiliaryData: { category: 'Web framework' } },
]

const source = createStaticSource(frameworks)
const value = ref<FrameworkItem | null>(null)
</script>

<template>
  <div class="base-typeahead-field">
    <BaseTypeahead
      v-model="value"
      :search-source="source"
      label="Search frameworks"
      placeholder="Search frameworks…"
      :debounce-ms="0"
      has-entries-on-focus
    >
      <template #icon>
        <span class="i-carbon-search base-typeahead-field__icon" aria-hidden="true" />
      </template>
      <template #item="{ item }">
        <VStack :gap="0" class="base-typeahead-field__result">
          <span class="base-typeahead-field__label">{{ item.label }}</span>
          <span class="base-typeahead-field__category">{{ item.auxiliaryData.category }}</span>
        </VStack>
      </template>
    </BaseTypeahead>
  </div>
</template>

<style scoped>
.base-typeahead-field {
  box-sizing: border-box;
  width: 22rem;
  max-width: 100%;
}

.base-typeahead-field__icon {
  display: block;
  width: 16px;
  height: 16px;
  color: var(--astryx-color-text-secondary, #525252);
}

.base-typeahead-field__result {
  min-width: 0;
}

.base-typeahead-field__label {
  overflow: hidden;
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.base-typeahead-field__category {
  overflow: hidden;
  color: var(--astryx-color-text-secondary, #525252);
  font-size: 0.75rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `T \| null` | `null` | Currently selected item. An empty value means nothing is selected yet. |
| `searchSource` | `SearchSource` | — | Static or async source with `search` and optional `bootstrap`. Required. |
| `label` | `string` | — | Accessible name of the input. Omit it when an outside `<label for>` already names 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. |
| `placeholder` | `string` | `Search…` | Placeholder text, shown while nothing is selected. |
| `disabled` | `boolean` | `false` | Disables the input. |
| `disabledReason` | `string` | — | Why the input is disabled, shown as a tooltip and announced as the description while the input stays focusable. |
| `isFocusableDisabled` | `boolean` | `false` | Keeps a disabled input focusable through `aria-disabled` and `readonly` so a caller-owned disabled reason stays discoverable. Text entry stays blocked. |
| `status` | `'default' \| 'success' \| 'warning' \| 'error'` | `'default'` | Border color of the control. The message itself belongs to the caller's field. |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the input and of the dropdown rows. |
| `clearable` | `boolean` | `false` | Shows a clear button. |
| `debounceMs` | `number` | `150` | Wait after typing before searching. Use 0 for local data. |
| `maxMenuItems` | `number` | `10` | Maximum rows in the dropdown. |
| `menuWidth` | `number` | — | Requested dropdown width in pixels. Never narrower than the anchor, and clamped to the viewport. |
| `minQueryLength` | `number` | `1` | Shortest query that gets searched, counted in visible characters. Below it nothing is searched and the menu stays closed. |
| `showOnFocus` | `boolean` | `false` | Shows bootstrap results on focus before typing. Legacy name of `hasEntriesOnFocus`; either one turns it on. |
| `hasEntriesOnFocus` | `boolean` | `false` | Shows bootstrap results on focus before typing. |
| `hasAutoFocus` | `boolean` | `false` | Focuses the input on mount. |
| `emptyText` | `string \| null` | — | Text shown when the source has nothing at all. |
| `emptySearchText` | `string \| null` | localized “No results found” | Text shown when the query matched nothing. `null` counts as not given and falls through to the default; pass an empty string to render nothing. |
| `required` | `boolean` | `false` | Marks the input as required. |
| `name` | `string` | — | Form field name. Submits the selected item's id. |
| `inputTabIndex` | `number` | — | Tab order of the input. |

## Events

- `update:modelValue(value: T | null)` / `change(value: T | null)`: the selection was set or cleared.
- `search(query: string)` / `changeQuery(query: string)`: the query text changed. Both fire; `search` is the legacy name.
- `openChange(isOpen: boolean)`: the dropdown opened or closed.

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.

## Slots

- `icon`: artwork at the start of the input. The wrapper is hidden from assistive technology.
- `item`: replaces the default row, and receives `item`, `isActive`, and `isSelected`. The row wrapper that owns highlight, selection, pointer, and keyboard behavior stays.
- `empty`: replaces the no-results content.
