GrAutocomplete

  • core
  • forms
  • API · 36 props
  • ⌨ keys

A combobox with type-ahead search over options, async loading and multi-select chips.

import { GrAutocomplete } from '@feugene/granularity/components/GrAutocomplete'

Overview

When to take it

  • there are too many options for a list — users, cities, tags, repositories: they are searched by typing, not browsed;
  • the list comes from a server — source and debounce together with the “searching” and “nothing found” states;
  • a minimum number of characters is needed — minQueryLength keeps the first letter from reaching the server;
  • there are several values — multiple shows the selection as chips, each removable from the keyboard;
  • the value may be missing from the reference list — allowCustomValue permits one of your own.

When to take something else

NeedTake
There are 5–50 options and they are browsed, not searchedGrSelect
The options are a tree with levelsGrTreeSelect
An application command is being searched, not a field valueGrCommandPalette
There is no reference list at all, strings are typedGrInputTag
There are 2–5 options and they are visible at onceGrSegmented

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 5 each with a live demo and its code

  • Found by whatever you remember

    Options come in groups — recent customers first — in the same shape GrSelect takes. A custom filter matches the name, the email or the VAT number, and the #option slot shows what tells two similar customers apart. #prefix holds the search icon, clearable empties the field.

    New invoice

    Name, email or VAT number

    Northwind TradersHamburg · DE 812 345 678 · [email protected]
  • Asking the server, not on every key

    fetch-options owns the round trip: it waits debounce ms after the last key, does not ask before min-query-length letters and cancels the stale request through the AbortSignal. The #loading and #empty slots give the waiting and the empty answer their own words, announced to a screen reader.

    Request a reviewQ3 VAT return · due 10 October
    Type at least 2 characters
  • Several labels, one nobody has made yet

    multiple shows the choice as chips — tag-tone and tag-radius give them the shape of labels — and close-on-select off keeps the panel open while picking. allow-custom-value adds «Add …» for a new label. With name the hidden inputs send the values, not the typed text.

    Export fails for reverse-charge invoices#1824 · opened by Grace Wu
    bugvat

    Sent with the form: nothing

  • Five thousand products, a window in the DOM

    A catalogue nobody scrolls through and the DOM should not carry either: virtual keeps only a window of rows around the viewport, and dropdown-max-height sets that window. A filter that takes every word on its own finds «walnut desk», and the #option slot puts the price on the right of each row.

    Add an order line5,000 products in the catalogue

API generated from the package · cannot drift

Props

PropTypedefaultDescription
modelValuerequiredv-modelGrAutocompleteModelValue<TValue>—The selected value (single — a string, multiple — an array of strings).
optionsGrAutocompleteOptionOrGroup<TValue>[] | undefinedundefinedThe available options. In the local mode — the full list (filtered on the client). In the remote mode (`filterable=false`) — the list the parent updates in response to the `search` event.
multipleboolean | undefinedfalseSelection of several values: the model is an array, and the selection sits in the field as chips. `false` by default.
tagTone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
"neutral"The look of the chips of the selected values in the `multiple` mode. They are drawn by `GrChip`, but the scale of the prop stayed the badge one: `tagSize` is public, and its values are obliged to mean the same type size as before. The translation of the steps is in `chipSizeForBadgeScale`.
tagDarkboolean | undefinedfalseThe chips of the selection are filled with the tone rather than sitting on a soft background — for a busy background.
tagSize"xs" | "sm" | "md" | "lg" | undefined"sm"The type size of the chips on the badge scale. `sm` by default: a chip lives inside the field, not in text.
tagRadiusGrBadgeRadius | undefined"round"The shape of the chips: `round` — a pill, `semi` — a tag, `square` — a stamp. `round` by default.
disabledboolean | undefinedfalseThe field is unavailable: the panel does not open, options cannot be selected, chips cannot be removed. Inherited from `GrFormField`.
readonlyboolean | undefinedfalseRead-only: the value is visible and leaves with the form, but is not edited.
invalidboolean | undefinedfalseThe visual and ARIA state of an error.
requiredboolean | undefinedfalseA required field (`aria-required`).
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe height and type size of the field on the control scale. `md` by default; taken from `GrConfigProvider`.
placeholderstring | undefinedundefinedHint in an empty field. In `multiple` with selected values it is hidden: the chips take up the room.
ariaLabelstring | undefinedundefinedThe accessible name of the field when there is no visible label. Inside `GrFormField` the name comes from its `label`.

Slots

SlotTypeDescription
prefixanyAn addon to the left of the input field.
suffixanyAn addon on the right, before the spinner and the cross.
option{ option: GrAutocompleteOption<TValue>; selected: boolean; }A row of the list instead of the label of an option.
loadinganyThe content of the panel while the options are on their way.
emptyanyThe content of the panel when there are no matching options.

Events

EventSignatureDescription
update:modelValue(e: "update:modelValue", value: GrAutocompleteModelValue<TValue>): voidThe new value (`v-model`): a string or, in `multiple`, an array.
search(e: "search", query: string): voidThe debounced search query — the way in for the remote loading of options.
searchError(e: "searchError", error: unknown): voidThe `fetchOptions` request failed (a cancellation of a stale one does not arrive here).
change(e: "change", value: GrAutocompleteModelValue<TValue>): voidThe value was committed by selecting or removing an option.
update:open(e: "update:open", value: boolean): voidThe panel opened or closed (`v-model:open`).
clear(e: "clear"): voidThe value was removed with the clear button; only with `clearable`.
focus(e: "focus", event: FocusEvent): voidFocus entered the control. Moving between the field and its panel does not count as an event.
blur(e: "blur", event: FocusEvent): voidFocus left the control — the field and the panel together.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrAutocompleteConfigurableProps, GrAutocompleteSize, GrAutocompleteOption } from '@feugene/granularity/components/GrAutocomplete'

Filedist/types/components/GrAutocomplete/config.d.ts

grAutocompleteConfigconst
const grAutocompleteConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrAutocomplete">;

Filedist/types/components/GrAutocomplete/defaults.d.ts

GrAutocompleteConfigurablePropsinterface
interface GrAutocompleteConfigurableProps {
    size: GrAutocompleteSize;
    clearable: boolean;
}

Filedist/types/components/GrAutocomplete/grAutocompleteStyles.d.ts

GrAutocompleteSizetype
type GrAutocompleteSize = GrComponentSize;
GrAutocompleteOptiontype
type GrAutocompleteOption<TValue extends GrAutocompleteValue = string> = {
    value: TValue;
    label: string;
    disabled?: boolean;
};
GrAutocompleteOptionGrouptype
type GrAutocompleteOptionGroup<TValue extends GrAutocompleteValue = string> = {
    label: string;
    options: GrAutocompleteOption<TValue>[];
};
GrAutocompleteOptionOrGrouptype
type GrAutocompleteOptionOrGroup<TValue extends GrAutocompleteValue = string> = GrAutocompleteOption<TValue> | GrAutocompleteOptionGroup<TValue>;
GrAutocompleteModelValuetype
type GrAutocompleteModelValue<TValue extends GrAutocompleteValue = string> = TValue | TValue[];

Filedist/types/components/GrAutocomplete/GrAutocomplete.vue.d.ts

GrAutocompletePropsinterface
interface GrAutocompleteProps<TValue extends GrAutocompleteValue = string> {
    modelValue: GrAutocompleteModelValue<TValue>;
    options?: GrAutocompleteOptionOrGroup<TValue>[];
    multiple?: boolean;
    tagTone?: GrBadgeTone;
    tagDark?: boolean;
    tagSize?: GrBadgeSize;
    tagRadius?: GrBadgeRadius;
    disabled?: boolean;
    readonly?: boolean;
    invalid?: boolean;
    required?: boolean;
    size?: GrAutocompleteSize;
    placeholder?: string;
    ariaLabel?: string;
    clearable?: boolean;
    loading?: boolean;
    filterable?: boolean;
    filter?: (option: GrAutocompleteOption<TValue>, query: string) => boolean;
GrAutocompleteEmitsinterface
interface GrAutocompleteEmits<TValue extends GrAutocompleteValue = string> {
    (e: 'update:modelValue', value: GrAutocompleteModelValue<TValue>): void;
    (e: 'search', query: string): void;
    (e: 'searchError', error: unknown): void;
    (e: 'change', value: GrAutocompleteModelValue<TValue>): void;
    (e: 'update:open', value: boolean): void;
    (e: 'clear'): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
}

Filedist/types/components/GrAutocomplete/safelist.d.ts

grAutocompleteSafelistconst
const grAutocompleteSafelist: string[];

Filedist/types/components/GrAutocomplete/index.d.ts

GrAutocompleteInstancetype
type GrAutocompleteInstance = ComponentExposed<typeof GrAutocompleteComponent>;

Accessibility

APG pattern

combobox (editable)

Keys

←Delete / BackspaceEsc

Usage from the library prose

Groups of options
vue
<GrAutocomplete
  v-model="city"
  :options="[
    { label: 'Europe', options: [{ value: 'lis', label: 'Lisbon' }] },
    { label: 'Asia', options: [{ value: 'tyo', label: 'Tokyo' }] },
    { value: 'remote', label: 'Remote' },
  ]"
/>

The shape of the data is the same as in GrSelect: two neighbouring controls whose lists differ only in the way of input are obliged to accept data the same way. Groups and single options mix in one array.

The filter hides a group as a whole if nothing in it matched. A heading over emptiness reads as a failure rather than as “there is nothing here”.

The keyboard walks the options only. The heading of a group is not selected with the arrows and does not enter the set for the screen reader: the direct children of role="listbox" are obliged to be options, so the heading lies inside its group and gives it a name through aria-labelledby. With virtualisation the window may start below the heading — then the name of the group goes into aria-label, and the group stays named all the same.

The same value in different groups is not an error: the key of a row is assembled with the index of the group, so two such options do not collapse into a single node, and hovering with the mouse highlights the one that is hovered.

Groups come from the server as well: fetchOptions is entitled to return the same array with groups, and the signature of the set is computed from their content — a change of an option inside a group does not go unnoticed.

Arbitrary values

allowCustomValue adds an “Add …” option to the list. It is an option like any other and takes part in arrow navigation: without that, committing a value of your own from the keyboard was impossible — with a non-empty list Enter always went to the active option.

The value is typed as text, so it is a string by nature: with a numeric TValue the branch does not apply.

Chips (multiple)

The crosses on the chips are deliberately not tabbable: in a combobox the focus lives on the <input>, and twenty selected values must not produce twenty Tab stops. Navigation is by arrows: ← from an empty query moves to the last chip, ←/→ walk between them, Delete/Backspace removes one, and Esc, → from the last one and any printable character return to the field.

Backspace in an empty field still removes the last chip without entering navigation.

What the panel holds

The direct children of role="listbox" are options only: the role declares everything else an invalid child. Loading, “type N more characters” and “nothing found” live below the list in a single live region role="status" aria-live="polite" — otherwise the asynchronous states would change silently.

The options do not take focus (mousedown is suppressed), so selecting with the mouse leaves the caret in the field — the panel does not reopen and the tab order is not disturbed.

The height of the panel

dropdownMaxHeight is the ceiling of the list, not of the panel. The panel itself is bounded by the room left to the edge of the viewport: the layer reports the measurement in --gr-floating-available-height, and on a short screen it is the list that shrinks — the input field and the status row stay in view, and there is a single scrollbar. The mechanism is shared across the list panels of the package and is described in ../overlays.md.

Virtualisation

virtual keeps in the DOM only the window around the viewport — its height is set by dropdownMaxHeight or by the available room, if there is less of it. There is one scenario it is meant for: a remote search across a reference list with thousands of matches.

vue
<GrAutocomplete v-model="city" :options="cities" virtual />

Three consequences worth knowing in advance.

The size of the set is declared explicitly. In the ordinary mode a screen reader derives it from the DOM, but with an incomplete set it would say “1 of 12” about a list of ten thousand. That is why with virtual the options carry aria-setsize and aria-posinset — for the whole filtered list, not for the window. In the ordinary mode those attributes are absent: there they would be noise.

The “Add …” row is a member of the set. With allowCustomValue it stands first and scrolls with the list rather than sticking to the top of the panel: the keyboard walks through it like through an ordinary option anyway.

The active option is always mounted. The arrows scroll the list to it before moving aria-activedescendant — otherwise the attribute would point at a node that is not in the DOM. How the primitive works and what it costs — virtual-list.md.

Programmatic control
ts
const box = useTemplateRef('box')
box.value?.focus()
box.value?.open()
box.value?.close()
States

disabled and readonly arrive both as props and from GrFormField. Both lock the control the same way: the panel does not open, options are not selected, chips are not removed, there is no clear button. The difference is only in semantics — readonly gives its value to the form and is announced as aria-readonly.

Slots
SlotWhat it replaces
prefix / suffixthe addon in the field shell
optionthe content of an option (option, selected)
loadingthe loading row
emptythe “nothing found” text
The prefix / suffix addons

The slots put an icon, a unit or a label into the shell; the width is bounded by six props (prefixMinWidth/prefixMaxWidth/prefixFixed and the same for the suffix). The shared contract of the controls — form-controls.md.

Panel control and the native form

The panel is controllable through v-model:open (the shared contract of panel overlays, as in GrPopover): without the open prop the previous uncontrolled behaviour holds, with it the parent owns the state and update:open accompanies every change.

The name prop enables participation in a native form: hidden inputs serialise the value of the model, not the text of the query — one per value with multiple, nothing when the selection is empty.

minQueryLength and the starting list in remote mode

While the query is shorter than minQueryLength, the panel shows the “type N more characters” hint rather than the results of the previous query — a stale list below the hint would misinform. With allowCustomValue the hint stands next to the “Add …” row: that row explains what to do with what has been typed, but not where the options went.

In remote mode (fetchOptions) the options prop is the starting list until the first answer from the server. Replacing it in the parent makes it the source again until the next answer and cancels the request in flight: that one belongs to the previous data set. The change is tracked by content, not by the identity of the array, so an inline literal :options="[...]" is safe — a list recreated on re-render with the same options does not touch the remote results.