# AutoComplete

Type to get suggestions: **the value is free text** (browser address bar / datalist mental model) — the suggestion list only aids input. Keyboard selection, grouping, debounced remote suggestions, and custom rendering are supported.

> **Boundary vs. Select(searchable) / Combobox**
>
> | Component | Value source | Input form | Best for |
> | --- | --- | --- | --- |
> | `oas-auto-complete` | **Any free text**; suggestions are shortcuts | Input is the control | Search-box suggestions, email completion, free-form tags |
> | `oas-select searchable` | Must come from the option set | Button-triggered; search box appears when expanded | Constrained choice (value must be valid) |
> | `oas-combobox` | Must come from the option set | Input is the control; typing only filters | Always-visible-input constrained single select |
>
> When the value must be one of the options, use select(searchable) or combobox instead of trying to add "strict validation" to auto-complete; for multiple/tags scenarios use `oas-select multiple` or `oas-dynamic-tags` — auto-complete stays single-value free text.

## Basic Usage

<DemoBlock title="Basic usage">
  <oas-auto-complete placeholder='Type "ap" to try' options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"},{"label":"Strawberry","value":"strawberry"}]'></oas-auto-complete>
</DemoBlock>

Typing auto-filters matching items in the dropdown; the first item is highlighted by default and `Enter` commits it directly; `↑`/`↓` move the highlight, `Esc` closes; **blur does not clear the input** (free text never falls back).

## Preset Value

<DemoBlock title="Preset value">
  <oas-auto-complete value="Apple" placeholder="Selected value" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
</DemoBlock>

Reading/writing the current value goes through the public `value` property (`el.value` to read, `el.value = x` to write, matching native `input.value`). Note the **label / value split**: this component's submission semantics are the *visible text* — after selecting a suggestion, `el.value` returns that suggestion's **label** (visible text), while the `value` **attribute** is the `option.value` (e.g. `apple`); `oas-form`'s `values` snapshot reads the attribute (= `option.value`). The two are different layers — pick per need.

## Clearable

<DemoBlock title="Clearable">
  <oas-auto-complete id="ac-clear" clearable value="Apple" placeholder="Clearable" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
</DemoBlock>

A clear button appears when the input has content; clicking clears the input and dispatches `oas-clear` and `oas-change` (empty value).

## Remote Suggestions (debounce + loading)

<DemoBlock title="Remote search suggestions">
  <oas-auto-complete id="ac-remote" debounce="300" clearable placeholder="Type a fruit name (simulated remote request)" options='[]'></oas-auto-complete>
  <span id="ac-remote-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 200px"></span>
</DemoBlock>

`debounce` (ms, default `0` = off) only debounces `oas-input` dispatching and filtering — **the input display is never debounced**; the host sets `loading` while requesting, the dropdown shows a loading placeholder, and refilling `options` renders the suggestions.

## Show on Focus (trigger-on-focus)

<DemoBlock title="Show all suggestions on focus (trigger-on-focus)">
  <oas-auto-complete trigger-on-focus placeholder="Focus to show all suggestions" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
</DemoBlock>

Off by **default** (popping the full list on focusing an empty input disturbs compact forms); enabling it follows the datalist mental model: focus shows suggestions filtered by the current input.

## Grouped Suggestions

<DemoBlock title="Groups (group)">
  <oas-auto-complete placeholder="Type a keyword" options='[{"group":"Digital products","label":"iPhone","value":"iphone"},{"group":"Digital products","label":"Apple Watch","value":"watch"},{"group":"Food & drinks","label":"Banana Milk","value":"banana-milk"},{"group":"Food & drinks","label":"Orange Juice","value":"orange"}]'></oas-auto-complete>
</DemoBlock>

Options with a `group` field render group titles (not selectable) with indented options; `↑`/`↓` navigate continuously across groups.

## Large Datasets (virtual scrolling)

<DemoBlock title="Virtual scrolling (1000 suggestions)">
  <oas-auto-complete id="ac-virtual" item-height="36" virtual clearable placeholder='Type "9" to try (1000 suggestions, smooth scrolling)' options='[]'></oas-auto-complete>
  <span id="ac-virtual-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

With `virtual`, the suggestion panel renders only the visible window (reusing `oas-virtual-list` window math with top/bottom padding holding the scroll height) — thousands of suggestions scroll smoothly and the panel height stays constant; `item-height` tunes the fixed row height (default `36`). While navigating with `↑`/`↓` the window follows the highlighted item and `aria-activedescendant` keeps pointing at a visible row; options with `group` automatically fall back to full rendering (same contract as `oas-select`).

## Custom Option Rendering

<DemoBlock title="Custom option (oas-option-render event)">
  <oas-auto-complete id="ac-render" placeholder="Type to try (rich suggestions)" options='[{"label":"Zhang Wei","value":"zhangwei"},{"label":"Wang Fang","value":"wangfang"},{"label":"Li Na","value":"lina"}]'></oas-auto-complete>
</DemoBlock>

<DemoBlock title="Custom option (template slot)">
  <oas-auto-complete id="ac-tpl" placeholder="Type to try (template skeleton)" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'>
    <template slot="option"><span>Fruit: </span><span data-option-label></span></template>
  </oas-auto-complete>
</DemoBlock>

The dual channel matches `oas-select`: listen to `oas-option-render` (`detail.element` is the text container the host can rewrite into icons/rich text); or provide a static skeleton via `<template slot="option">` whose `[data-option-label]` node auto-binds the option text.

## Panel Slots (header / footer / empty)

<DemoBlock title="Panel header/footer and empty slots">
  <oas-auto-complete id="ac-slots" placeholder='Type "pe" to try (custom empty)' options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'>
    <span slot="header">Hint: try a fruit name</span>
    <span slot="footer">View all fruits →</span>
    <span slot="empty">Try another keyword</span>
  </oas-auto-complete>
</DemoBlock>

`header` / `footer` slots render at the top/bottom of the suggestion panel (hint bar, "view all" link); the `empty` slot replaces the default "no match" text.

## Size & Status

<DemoBlock title="Sizes (size)">
  <oas-space size="small" direction="vertical">
    <oas-auto-complete size="small" placeholder="small" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
    <oas-auto-complete placeholder="medium (default)" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
    <oas-auto-complete size="large" placeholder="large" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
  </oas-space>
</DemoBlock>

<DemoBlock title="Status (status)">
  <oas-space size="small">
    <oas-auto-complete status="success" value="Apple" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
    <oas-auto-complete status="warning" value="Unknown fruit" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
    <oas-auto-complete status="error" value="Unknown fruit" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
  </oas-space>
</DemoBlock>

`size` supports `small / medium / large` (follows the nearest `oas-config-provider` `size` injection); `status` supports `success / warning / error` (`error` syncs `aria-invalid`, drivable by `oas-form-item` validation).

## Variant

<DemoBlock title="Variant (variant)">
  <oas-space size="small" direction="vertical">
    <oas-auto-complete variant="outlined" placeholder="outlined (default)" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'></oas-auto-complete>
    <oas-auto-complete variant="filled" placeholder="filled (gray fill)" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'></oas-auto-complete>
    <oas-auto-complete variant="borderless" placeholder="borderless (no border)" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'></oas-auto-complete>
  </oas-space>
</DemoBlock>

`variant` aligns with the three `oas-input` variants: `outlined` (default) / `filled` (gray fill, border on hover) / `borderless` (no border, no focus shadow); invalid values fall back to `outlined`.

## Focus Events (oas-focus / oas-blur)

<DemoBlock title="focus / blur events">
  <oas-auto-complete id="ac-focus-blur" placeholder="Focus / blur to see feedback" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
  <span id="ac-focus-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

Focus/blur dispatch `oas-focus` / `oas-blur` with `detail: { value }` carrying the current input text, matching the `oas-input` event contract.

## Readonly

<DemoBlock title="Readonly">
  <oas-auto-complete readonly value="Apple" placeholder="Readonly" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'></oas-auto-complete>
</DemoBlock>

`readonly` allows focus & copy but no editing; focus and keyboard no longer open suggestions.

## IME (Input Method Editor)

During IME composition (before confirming the candidate) the component **does not filter, does not dispatch `oas-input`, and Enter never mis-selects a suggestion**; after composition ends, one filtered update fires with the final text. Try typing Chinese in any demo on this page — no extra configuration needed.

## Disabled

<DemoBlock title="Disabled">
  <oas-auto-complete disabled placeholder="Not editable" options='[{"label":"Apple","value":"apple"}]'></oas-auto-complete>
</DemoBlock>

## No Match

<DemoBlock title="Empty state">
  <oas-auto-complete placeholder='Type "pe" to try (no match)' options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
</DemoBlock>

## Events

<DemoBlock title="Input & selection events">
  <oas-auto-complete id="ac-event" clearable placeholder="Type or select" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]'></oas-auto-complete>
  <span id="ac-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

Listen to `oas-input` (typing, after debounce), `oas-change` (select or clear), `oas-clear` (clear):

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  const el = document.getElementById('ac-event')
  const out = document.getElementById('ac-output')
  const set = (name, e) => {
    out.textContent = `${name}: ${JSON.stringify(e.detail)}`
  }
  el?.addEventListener('oas-input', (e) => set('oas-input', e))
  el?.addEventListener('oas-change', (e) => set('oas-change', e))
  el?.addEventListener('oas-clear', (e) => set('oas-clear', e))

  // focus / blur demo: show the latest focus event inline
  const fb = document.getElementById('ac-focus-blur')
  const fbOut = document.getElementById('ac-focus-output')
  const setFb = (name, e) => {
    fbOut.textContent = `${name}: ${JSON.stringify(e.detail)}`
  }
  fb?.addEventListener('oas-focus', (e) => setFb('oas-focus', e))
  fb?.addEventListener('oas-blur', (e) => setFb('oas-blur', e))

  // remote suggestions demo: simulate a host request (300ms debounce handled by the component)
  const remote = document.getElementById('ac-remote')
  const remoteOut = document.getElementById('ac-remote-output')
  const ALL = ['Apple', 'Banana', 'Orange', 'Strawberry', 'Watermelon', 'Grape', 'Mango'].map((label, i) => ({
    label,
    value: `v${i}`,
  }))
  let timer = 0
  remote?.addEventListener('oas-input', (e) => {
    const q = e.detail.value
    window.clearTimeout(timer)
    remote.setAttribute('loading', '')
    remoteOut.textContent = `Requesting: ${q}`
    timer = window.setTimeout(() => {
      remote.removeAttribute('loading')
      remote.setAttribute('options', JSON.stringify(ALL.filter((o) => o.label.includes(q))))
      remoteOut.textContent = `Refilled ${q ? 'matched' : 'full'} suggestions`
    }, 500)
  })

  // rich suggestion demo: rewrite element via oas-option-render (name + email, two lines)
  const render = document.getElementById('ac-render')
  const EMAILS = { zhangwei: 'zhangwei@example.com', wangfang: 'wangfang@example.com', lina: 'lina@example.com' }
  render?.addEventListener('oas-option-render', (e) => {
    const { option, element } = e.detail
    if (!element) return
    element.textContent = ''
    const name = document.createElement('div')
    name.textContent = option.label
    const mail = document.createElement('div')
    mail.textContent = EMAILS[option.value] ?? ''
    mail.style.fontSize = 'var(--oas-font-size-xs)'
    mail.style.color = 'var(--oas-color-text-secondary)'
    element.append(name, mail)
  })

  // autofocus demo: focused on mount (native autofocus doesn't pierce Shadow DOM; forwarded to the inner input)
  const acFocusBtn = document.getElementById('ac-focus-btn')
  const acFocusZone = document.getElementById('ac-focus-zone')
  acFocusBtn?.addEventListener('click', () => {
    if (!acFocusZone) return
    acFocusZone.innerHTML = ''
    const ac = document.createElement('oas-auto-complete')
    ac.setAttribute('autofocus', '')
    ac.setAttribute('placeholder', 'Focused on mount (caret inside)')
    ac.setAttribute('options', '[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"}]')
    acFocusZone.appendChild(ac)
  })

  // virtual scrolling demo: 1000 suggestions injected via the options channel + selection feedback
  const virtualAc = document.getElementById('ac-virtual')
  const virtualAcOut = document.getElementById('ac-virtual-output')
  virtualAc?.setAttribute(
    'options',
    JSON.stringify(Array.from({ length: 1000 }, (_, i) => ({ label: `Option ${i}`, value: `v${i}` }))),
  )
  virtualAc?.addEventListener('oas-change', (e) => {
    if (virtualAcOut) virtualAcOut.textContent = `oas-change: ${JSON.stringify(e.detail)}`
  })
})
</script>

## required

<DemoBlock title="required (native validation chain)">
  <oas-auto-complete required name="fruit" placeholder="Type a fruit" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"}]'></oas-auto-complete>
</DemoBlock>

`required` drives the native validation chain (form-associated): when unfilled, `checkValidity()` returns false (`valueMissing`) and native form submission is blocked; it recovers to `:valid` once filled.

Programmatic read/write of the current value goes through the public `value` property (like the native `input.value`): `el.value` reads the current input text; `el.value = x` writes the controlled `value` attribute and immediately syncs the inner control (clearing the in-progress filter query) without dispatching events.

## Panel placement

`placement` controls the suggestion panel placement in 12 directions (aligned with the floating-positioning engine; `-start` / `-end` are logical writing-direction semantics, mirrored automatically in RTL): flips along the main axis when space is short and avoids viewport edges. Default `bottom-start` (panel's start edge aligned with the input's start edge, opening downward); the actual placement (including flips) is written to the panel's `data-placement`.

<DemoBlock title="placement=top-start (opens upward)">
  <oas-auto-complete placement="top-start" placeholder="Type a fruit (panel opens upward)" options='[{"label":"Apple","value":"apple"},{"label":"Banana","value":"banana"},{"label":"Orange","value":"orange"},{"label":"Grape","value":"grape"}]'></oas-auto-complete>
</DemoBlock>

## autofocus

`autofocus` is forwarded to the inner input (native autofocus doesn't pierce Shadow DOM): the component focuses once after mounting. Click the button below to mount a fresh `autofocus` instance and watch the caret land inside the input.

<DemoBlock title="Focused on mount">
  <oas-button id="ac-focus-btn" size="small">Mount an autofocus input</oas-button>
  <div id="ac-focus-zone" style="margin-top: var(--oas-space-2)"></div>
</DemoBlock>

## API

### oas-auto-complete

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `autofocus` | Focuses the inner input after mounting | `boolean` | — |
| `clearable` | Clearable (shows a clear button when the input has content; clearing dispatches `oas-clear` and an empty `oas-change`) | `boolean` | — |
| `debounce` | Input debounce (ms, default 0 = off): only debounces `oas-input` dispatch and filtering, never the input display | `string` | `0` |
| `disabled` | Disabled | `boolean` | — |
| `item-height` | Fixed virtual row height in px (with virtual, default 36) | `string` | `36` |
| `loading` | Loading placeholder (dropdown shows a loading state; remote-suggestion request state) | `boolean` | — |
| `options` | Options, JSON array `[{ label, value, disabled?, group? }]` (group is the group title) | `Option[] \| string` | `[]` |
| `placeholder` | Placeholder text | `string` | — |
| `placement` | 12-direction panel placement (engine flip/RTL result written to data-placement) | `string` | `bottom-start` |
| `readonly` | Readonly (focusable & copyable, not editable; focus and keyboard never open suggestions) | `boolean` | — |
| `required` | Required marker (drives the native valueMissing validation chain; not passed through to the inner control) | `boolean` | — |
| `size` | Size tier: small / medium / large (default medium, follows the nearest config-provider injection) | `string` | `medium` |
| `status` | Validation status: success / warning / error (error syncs aria-invalid, drivable by oas-form-item validation) | `string` | — |
| `trigger-on-focus` | Show suggestions on focus (datalist mental model; off by default — input-first) | `boolean` | — |
| `value` | Preset value | `string` | — |
| `variant` | Visual variant: `outlined` (default) / `filled` / `borderless`; invalid values fall back silently | `string` | — |
| `virtual` | Virtual scrolling (renders only the visible window via oas-virtual-list; grouped options fall back to full rendering) | `boolean` | — |

#### Property (JS property; see each row for attribute reflection)

| Property | Description | Type | Default |
| --- | --- | --- | --- |
| `value` | Current value: the visible text (the selected suggestion label after choosing); the `value` attribute is the `option.value` | `string` | — |

#### Events

| Event | Description |
| --- | --- |
| `oas-blur` | Dispatched on blur with the current input text, `detail: { value: this.input?.value ?? '' }` |
| `oas-change` | Selected or cleared, `detail: { value, label }` |
| `oas-clear` | Clear button clicked, `detail: { value }` (value before clearing) |
| `oas-focus` | Dispatched on focus with the current input text, `detail: { value: this.input?.value ?? '' }` |
| `oas-input` | While typing (after debounce), `detail: { value }` |
| `oas-option-render` | Dispatched after each option row renders, `detail: { index, option, element }`; the host can rewrite `element` (icons/rich text) |

#### Slots

| Name | Description |
| --- | --- |
| `empty` | Custom no-match empty state (replaces the default text) |
| `footer` | Suggestion panel footer content (e.g. a view-all link) |
| `header` | Suggestion panel header content (e.g. a hint bar) |
| `template[slot="option"]` | Custom option template skeleton; the `[data-option-label]` node auto-binds the option text |

#### CSS Variables

| CSS Variable | Default |
| --- | --- |
| `--oas-auto-complete-dropdown-height` | `240px` |
| `--oas-glass-blur` | `none` |
| `--oas-glass-ring` | `transparent` |

Keyboard: `↑`/`↓` to move (looping), `Enter` to select the highlighted item (first item highlighted by default), `Esc` to close.
