autocomplete
- Grouped options•
- Custom filtering and external state synchronization•
- Fetch as you type•
- Multiple selection•
- Customizing the text field•
- Toggle and reset buttons•
- Customizing the selected options tags•
- Other input related props•
- Customizing the list box•
- Appearance and dividers•
- Widget•
- Customizing individual options•
- Events•
- Controlled states•
- Integrating with external libraries•
- React Hook Form•
- Formik•
- Using the Autocomplete to make a custom select
Props
| Prop name | Type | Default | Description |
|---|---|---|---|
extraClasses | string | undefined | Injected classes. | |
innerRef | Ref<HTMLDivElement> | undefined | React ref forwarded to the outer div. | |
expanded | boolean | undefined | Controls the listbox visibility and disables internal active state management. | |
options | ListBoxOptionsType<OptionType> | undefined | List of options to choose from. | |
value | OptionType[] | undefined | List of currently selected options. Makes the component controlled. | |
defaultValue | OptionType[] | undefined | [] | List of options that are initially selected. |
inputValue | string | undefined | Controls the input value. | |
defaultInputValue | string | undefined | Initial input value. | |
multiple | boolean | undefined | false | Enables selection of multiple options. |
debounce | number | undefined | 300 | Debounce time in milliseconds. |
listBoxProps | AutocompleteListBoxProps<OptionType> | undefined | Props forwarded to the options list, ListBox component. | |
comboboxProps | AutocompleteComboboxProps<OptionType> | undefined | Props forwarded to the text field, Combobox component. | |
onChange | ((selectedOptions: OptionType[], event?: SyntheticEvent<Element, Event> | undefined) => void) | undefined | Called back with an array of the currently selected options whenever this value changes. | |
onToggle | ((expanded: boolean, event?: SyntheticEvent<Element, Event> | undefined) => void) | undefined | Called back with the current expanded state whenever it changes. | |
onDebounce | typeof defaultAutocompleteFilter<OptionType> | ((inputValue: string, options: ListBoxOptionsType<OptionType>) => void | ListBoxOptionsType<OptionType>) | undefined | Fired when the input value changes (with debounce). Replaces internal filtering logic. Updates options list if either options array or a Promise is returned. |
The Autocomplete is responsible for orchestrating the interactions between Combobox and ListBox to create an input that can be filled with an option from a list of available options.
The user input on the Combobox filters the options available on the ListBox and the controller handles all necessary events and interactions.
The options prop accepts a list of all available options, and the structure is the one supported
by the ListBox component:
{
title: string
subtitle?: string
value: string
}[]By default, the component will filter the options by the contents of their title and subtitle.
This example shows a simple Autocomplete with title:
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <Autocomplete options={pokemons} comboboxProps={{ label: 'Pokémon', placeholder: 'Find your Pokémon...', }} />
Here is an example with title and subtitle. Notice how you can also find pokémons by typing their
types on the combobox field. That is because we created the subtitle property from their types.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const pokemonsWithTypeSubtitle = pokemons.map((pokemon) => ({ ...pokemon, // Adds `subtitle` property from each pokemon array of types subtitle: pokemon.types.join(', ') })); <Autocomplete options={pokemonsWithTypeSubtitle} comboboxProps={{ label: 'Pokémon with types', placeholder: 'Search pokémon title and subtitle...', }} />
Grouped options
To show options in a group you must change the format of the options prop. Group them in the following format instead:
{
group: string
options: {
title: string
subtitle?: string
value: string
}[]
}[]Optionally, you can customize the listbox group names by using the renderGroupName function in the
listBoxProps prop. This function will be called with the group as an argument and must return an
element.
⚠️ Please do not mix up grouped and ungrouped options as the component does not support mixed option formats.
import { Autocomplete } from '@flixbus/honeycomb-react'; import { Icon, IconTimeSolid } from '@flixbus/honeycomb-icons-react'; const groupedOptions = [ { group: 'Recent', options: [ { title: 'Pikachu', value: '25' }, ], }, { group: 'Grass', options: [ { title: 'Bulbasaur', value: '1' }, { title: 'Ivysaur', value: '2' }, { title: 'Venusaur', value: '3' }, ], }, { group: 'Fire', options: [ { title: 'Charmander', value: '4' }, { title: 'Charmeleon', value: '5' }, { title: 'Charizard', value: '6' }, ], }, { group: 'Water', options: [ { title: 'Squirtle', value: '7' }, { title: 'Wartortle', value: '8' }, { title: 'Blastoise', value: '9' }, ], }, ]; const renderGroupName = ({ group }) => { if (group === 'Recent') { return <><Icon InlineIcon={IconTimeSolid} aria-hidden="true" /> Recent</>; } return group; } <Autocomplete options={groupedOptions} comboboxProps={{ label: 'Grouped Pokémon' }} listBoxProps={{ renderGroupName }} />
Custom filtering and external state synchronization
If you're not satisfied with the default filtering, you can provide your own filtering function via
the onDebounce prop. This function receives the input value and the options array as arguments
and can return either a Promise that resolves to an array with the filtered options or an array itself.
This output will be displayed on the list box.
In this example we will implement search by Pokémon type using onDebounce callback to mutate options stored in external state.
By default, the filtering happens on both title and subtitle, but here
we only want to filter Pokémon by their types. To reflect that we set the highlightOptions prop to
subtitle, this will highlight only types (subtitles) without touching the names in the list box options list.
highlightOptions listBox prop allows you to customize where the component will apply the matches highlight, it can equal to title, subtitle, none or both.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const pokemonsWithTypeSubtitle = pokemons.map((pokemon) => ({ ...pokemon, // Adds `subtitle` property from each pokemon array of types subtitle: pokemon.types.join(', ') })); const [options, setOptions] = React.useState(pokemonsWithTypeSubtitle); const filterPokemonsByType = (inputValue) => { const filterRegex = new RegExp(inputValue, 'i'); const filteredPokemons = pokemonsWithTypeSubtitle.filter((pokemon) => pokemon.types.some((type) => type.match(filterRegex))); setOptions(filteredPokemons); }; <Autocomplete options={options} onDebounce={filterPokemonsByType} onChange={(selectedOptions) => {setOptions(selectedOptions)}} comboboxProps={{ label: 'Find Pokémon by types', placeholder: 'Grass, Fire, Water...', }} listBoxProps={{ highlightOptions: 'subtitle', }} />
Fetch as you type
Instead of filtering through a static list of options you can also use the onDebounce prop to
fetch new options as the user types. The function receives the input value as an argument and
must return a Promise that is resolved with the desired results. If you need to store the
options state on your side you can do this here.
In the example bellow we mimic an API that fetches a list of pokémons as the user types.
import React from 'react'; import { Autocomplete, Spinner } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const [loading, setLoading] = React.useState(false); const [options, setOptions] = React.useState([]); const fetchFromPokedex = (inputValue) => { setLoading(true); return new Promise((resolve) => { setTimeout(() => { const response = pokemons.filter((option) => option.title.match(new RegExp(inputValue, 'i'))); setLoading(false); setOptions(response); // resolve the promise with the API response resolve(response); }, 1000); }); }; <Autocomplete onDebounce={fetchFromPokedex} comboboxProps={{ label: 'Pokédex', placeholder: 'Find your Pokémon...', loading, 'aria-live': 'polite', srOnlyInfo: `${options.length} option${options.length === 1 ? '' : 's'}.` }} />
Multiple selection
To allow multiple selection, set the multiple prop.
When multiple selection is enabled, you may also provide the Confirm and Reset buttons to enhance
the user experience. This is done by using the actionButtonProps and resetButtonProps props.
The "Confirm" is a list box action and to enable it you must provide at least a label or children in
the listBoxProps prop.
You can also pass any other props that are supported by the Button component
and they will be forwarded as ...rest props. Including onClick handlers.
In addition to that, the optional onClick handler will be called with not only the event but also
the current selection as second argument.
The reset button is a combobox controller and will show when there are selected options.
To enable it you must provide at least a label for it in the comboboxProps prop. Other props will
be forwarded to the underlying close button component as ...rest props.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <Autocomplete multiple options={pokemons} comboboxProps={{ label: 'Multiple Pokémons', resetProps: { label: 'Clear', onClick: (event) => console.log('Reset button clicked!'), } }} listBoxProps={{ actionButtonProps: { children: 'Confirm', onClick: (event, selectedOptions) => { alert(`You have selected ${selectedOptions.length} pokemons.`) } }, }} />
Customizing the text field
With the comboboxProps prop you can customize the underlying Combobox component.
The combobox supports most Input props, except for the controllers (which is used for rendering
the Autocomplete reset and toggle buttons). In addition to that, the following props are available
for Combobox customization:
- `resetProps`
- { label: string } & HTMLButtonAttributes
- Props forwarded to the Reset button. Label is required for assistive technologies. If absent, the Reset button will not be rendered.
- `toggleProps`
- { label: string } & HTMLButtonAttributes
- Props forwarded to the Toggle button. Label is required for assistive technologies. If absent, the Reset button will not be rendered.
- `limitSelectedOptions`
- number
- Limits the amount of selected options displayed as tags.
- `tagProps`
- (option: ListBoxOptionType) => TagProps
- Function that must return an object with rest props for each of the selected options tag.
- `srOnlyInfo`
- string
- Info text used to announce changes for screen readers, e.g.: announcing changes in the number of available options.
Toggle and reset buttons
The toggle and reset buttons are optional and can be enabled by providing at least a label for them
in the comboboxProps.
You can also pass other Button props to the underlying Button component as ...rest props, and
they will be forwarded accordingly.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const [info, setInfo] = React.useState(''); <Autocomplete options={pokemons} defaultValue={[pokemons[0]]} defaultInputValue={pokemons[0].title} comboboxProps={{ label: 'Combobox with toggle and reset controllers', resetProps: { label: 'Reset', onClick: () => setInfo('Reset button clicked') }, toggleProps: { label: 'Toggle', onClick: () => setInfo('Toggle button clicked') }, info, }} />
Customizing the selected options tags
The tagProps prop is a function that must return an object with rest props for each of the tags.
The returned object will be forwarded to the underlying Tag component and accepts any valid Tag prop.
In addition to the Tag component props, you can also provide an additional prop renderTag
function that must return a ReactNode to be rendered as the tag children.
For example, let's add the Pokémon numbers as their tags instead of the titles:
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <Autocomplete multiple options={pokemons} defaultValue={[ { title: 'Pikachu', value: 25 }, pokemons[0], ]} comboboxProps={{ label: 'Combobox with custom tags', tagProps: (selectedOption) => ({ renderTag: () => <>#{String(selectedOption.value).padStart(3, '0')}</>, closeProps: { label: `Remove ${selectedOption.title}`, }, }), }} />
You can limit the number of selected options that are displayed when the component is not focused
by setting the limitSelectedOptions prop with the desired number of tags.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <Autocomplete multiple options={pokemons} defaultValue={pokemons} comboboxProps={{ label: 'Limited tags', limitSelectedOptions: 3, resetProps: { label: 'Clear', onClick: (event) => console.log('Selection reset!') }, tagProps: (option) => ({ closeProps: { label: `Remove ${option.title}`, }, }), }} listBoxProps={{ actionButtonProps: { children: 'Confirm', }, }} />
Other input related props
Most of the input component props are supported by the combobox as well, so if you want to add an
icon, inline labels and do validation, you can do it with the comboboxProps.
import { Autocomplete } from '@flixbus/honeycomb-react'; import { Icon, IconGhost } from '@flixbus/honeycomb-icons-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const [pokemon1, setPokemon1] = React.useState({}); const [pokemon2, setPokemon2] = React.useState({}); <> <Autocomplete options={pokemons} comboboxProps={{ label: '1. Combobox with icon', iconLeft: <Icon InlineIcon={IconGhost} aria-hidden="true" />, }} /> <Autocomplete options={pokemons} comboboxProps={{ label: '2. Value on inline label', inlineLabelLeft: (pokemon1 && pokemon1.value) ? `#00${pokemon1.value}`: undefined, }} onChange={([selection]) => setPokemon1(selection)} /> <Autocomplete options={pokemons} comboboxProps={{ label: '3. Combobox validation', valid: pokemon2 && pokemon2.title === 'Bulbasaur', infoError: 'You must pick Bulbasaur', }} onChange={([selection]) => setPokemon2(selection)} /> <Autocomplete options={pokemons} comboboxProps={{ disabled: true, label: '4. Disabled combobox', inlineLabelLeft: (pokemon1 && pokemon1.value) ? `#00${pokemon1.value}`: undefined, }} onChange={([selection]) => setPokemon1(selection)} /> </>
Customizing the list box
With the listBoxProps prop you can customize the underlying ListBox component.
The supported props are:
- `appearance`
- "boxed" (default) or "flat"
- `hideDividers`
- Hides dividers between options.
- `widget`
- React.ReactElement
- Widget rendered after the options list.
- `optionsToDisplay`
- Number of options to display before the list box starts scrolling. Default is 3. Set it to 0 to remove the height constraint.
- `actionButtonProps`
- Props forwarded to the action button, e.g.: "Confirm button". If no props are provided, the button will not be rendered.
- `optionProps`
- (option: ListBoxOptionType) => ListBoxOptionProps
- Function returning an object with the rest props for the LI element of each option.
Appearance and dividers
The default appearance for the options list is boxed and with visible dividers.
You can make it look like a flat list by setting the appearance prop to flat. The flat listbox
has square borders, no outer box shadow and a more subtle inner shadow.
You can also hide the dividers between the options by setting the hideDividers prop to true.
import { Autocomplete } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <> <Autocomplete comboboxProps={{ label: 'Flat listbox' }} options={pokemons} listBoxProps={{ appearance: 'flat' }} /> <Autocomplete comboboxProps={{ label: 'hideDividers' }} options={pokemons} listBoxProps={{ hideDividers: true }} /> </>
Widget
The widget prop can be used to render a widget after the list of options and before the confirm
button (if it is present).
For example:
import { Autocomplete, ListBox, ImageBox, spaceHelpers } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Growlithe', value: '58' }, { title: 'Arcanine', value: '59' }, ]; const { 2: space2 } = spaceHelpers(); <Autocomplete multiple comboboxProps={{ label: 'Listbox with a widget' }} options={pokemons} listBoxProps={{ actionButtonProps: { children: 'Confirm' }, optionsToDisplay: 0, widget: ( <ImageBox title="Growlithe" text="Friendly and loyal Pokémon that will fearlessly defend its Trainer and territory from harm." outlined small img={{ src: 'https://styleguide.hive.flix.tech/img/img-placeholder-grey.png', alt: 'Growlithe' }} extraClasses={`${space2.top} ${space2.right} ${space2.bottom} ${space2.left}`} /> ), }} />
You can use the widget area to display a message if there are no available options for a given
query. The onUpdate list box callback happens whenever the length of the options list changes.
Another use for these changes can be to announce the number of options for screen readers using the
combobox srOnlyInfo prop.
import React from 'react'; import { Autocomplete, ListBox, Box, spaceHelpers } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const [options, setOptions] = React.useState(pokemons); const { 2: space2 } = spaceHelpers(); // To keep options filtering between rerenders caused by listBoxProps/onUpdate callback state update, // we need to perform filtering externally const filterOptionsByTitle = (inputValue) => { const filterRegex = new RegExp(inputValue, 'i'); const filteredOptions = pokemons.filter((pokemon) => { return pokemon.title.match(filterRegex) }); setOptions(filteredOptions); return new Promise((resolve) => { resolve(filteredOptions); }); }; <Autocomplete comboboxProps={{ label: 'Options feedback', srOnlyInfo: `${options.length} option${options.length === 1 ? '' : 's'}.` }} onDebounce={filterOptionsByTitle} options={options} listBoxProps={{ actionButtonProps: { children: 'Confirm' }, widget: !options || options.length === 0 ? ( <Box small appearance="dimmed" extraClasses={`${space2.top} ${space2.right} ${space2.bottom} ${space2.left}`}> No options found. </Box> ) : null, }} />
Customizing individual options
The optionProps prop can be used to customize the rendering of the individual options and also
render option specific widgets, such as a clear or a toggle button.
The function will be called for each option and should return an object that contains the rest props forwarded to the option component. The supported component props are:
{
/**
* Function that can be used to customize the rendering of each option.
*/
renderOption?: ({ option, highlightQuery, shouldHighlightTitle, shouldHighlightSubtitle }) => React.ReactNode
/**
* Function that allows you to render a widget next to the option,
* such as a clear button or an icon.
*/
renderWidget?: (...args: unknown[]) => React.ReactNode
/** Modifier to show the option indented. */
indent?: boolean
}Notice that when you use the renderOption, the component's automatic highlighting will not work,
so you must handle it on your side. You can use the highlightText helper
to do it, or any other package you like that will add <mark> tags around the matches.
The other properties in the optionProps object will be forwarded as rest props to the options LI
element.
import React from 'react'; import { Autocomplete, Text, Heading, Tag, CloseButton, highlightText } from '@flixbus/honeycomb-react'; const [inputValue, setInputValue] = React.useState(''); const recent = [{ group: 'Recent', options: [ { value: '#001', line: 'bulb', title: 'Bulbasaur', types: ['Grass', 'Poison'], recent: true }, { value: '#004', line: 'char', title: 'Charmander', types: ['Fire'], recent: true }, { value: '#007', line: 'squi', title: 'Squirtle', types: ['Water'], recent: true }, ] }]; const pokemons = [ { value: 'bulb', line: 'bulb', title: 'Bulbasaur (all evolution line)' }, { value: '#001', line: 'bulb', title: 'Bulbasaur', types: ['Grass', 'Poison'] }, { value: '#002', line: 'bulb', title: 'Ivysaur', types: ['Grass', 'Poison'] }, { value: '#003', line: 'bulb', title: 'Venusaur', types: ['Grass', 'Poison'] }, { value: 'char', line: 'char', title: 'Charmander (all evolution line)' }, { value: '#004', line: 'char', title: 'Charmander', types: ['Fire'] }, { value: '#005', line: 'char', title: 'Charmeleon', types: ['Fire'] }, { value: '#006', line: 'char', title: 'Charizard', types: ['Fire', 'Flying'] }, { value: 'squi', line: 'squi', title: 'Squirtle (all evolution line)' }, { value: '#007', line: 'squi', title: 'Squirtle', types: ['Water'] }, { value: '#008', line: 'squi', title: 'Wartortle', types: ['Water'] }, { value: '#009', line: 'squi', title: 'Blastoise', types: ['Water'] }, ]; const renderOption = (pokemon) => ( <> <Heading Elem="span" size={4} flushSpace> {(pokemon.value !== pokemon.line) && highlightText(pokemon.value, inputValue)} {' '} {highlightText(pokemon.title, inputValue)} </Heading> {pokemon.types && ( <Text small> {pokemon.types.join(', ')} </Text> )} </> ); const renderWidget = (pokemon) => { if (!pokemon.recent) { return null; } return ( <CloseButton aria-label={`Remove ${pokemon.title} from recent searches`} onClick={(e) => { e.stopPropagation(); alert(`${pokemon.title} removed from recent searches`); }} /> ); }; const filterPokemonsByTitleAndValue = (inputValue) => { const filterRegex = new RegExp(inputValue, 'i'); if (!inputValue) { return new Promise((resolve) => resolve(recent)); } return new Promise((resolve) => { resolve(pokemons.filter((pokemon) => { return pokemon.title.match(filterRegex) || pokemon.value.match(filterRegex) })); }); }; <Autocomplete options={recent} multiple onDebounce={filterPokemonsByTitleAndValue} comboboxProps={{ label: 'Customized pokémon options', onChange: ({ target }) => { setInputValue(target.value) }, }} listBoxProps={{ optionsToDisplay: 0, optionProps: (option) => ({ renderOption: () => renderOption(option), renderWidget: () => renderWidget(option), indent: !option.recent && option.line !== option.value, 'data-testid': `pokemon-${option.value}`, }) }} />
Events
The autocomplete currently has 3 main event callbacks:
onDebounce: Fires with debounce when the combobox value (inputValue) changes.onToggle: Fires when the expanded state changes.onChange: Fires when the selected options (value) changes.
The underlying components also have their own events you can use.
The combobox accepts all the same events as an input component, the Autocomplete does not
intervene in the handling of onChange events, so please notice that value changes that happened
programmatically will not trigger the onChange event.
The list-box has 2 events related to state changes:
onChange: Called back when the list of selected options (value) changes.onUpdate: Called back when the list os options to be displayed changes.
Additionally, the list-box individual options accept an onChange callback that will be called
whenever each option selection state changes.
import React from 'react'; import { Autocomplete, Text } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const [value, setValue] = React.useState([]); const [inputValue, setInputValue] = React.useState(''); const [expanded, setExpanded] = React.useState(false); const [options, setOptions] = React.useState(pokemons); const [optionsLength, setOptionsLength] = React.useState(pokemons.length); const [lastSelectedOption, setLastSelectedOption] = React.useState(); const filterOptionsByTitle = (inputValue) => { const filterRegex = new RegExp(inputValue, 'i'); const filteredOptions = pokemons.filter((pokemon) => { return pokemon.title.match(filterRegex) }); setOptions(filteredOptions); return new Promise((resolve) => { resolve(filteredOptions); }); }; <> <Text><code>value</code>: <output>{value}</output></Text> <Text><code>inputValue</code>: <output>{inputValue}</output></Text> <Text><code>expanded</code>: <output>{`${expanded}`}</output></Text> <Text><code>optionsLength</code>: <output>{optionsLength}</output></Text> <Text><code>lastSelectedOption</code>: <output>{lastSelectedOption}</output></Text> <Autocomplete multiple options={options} onToggle={(expanded) => setExpanded(expanded)} onChange={(selectedOptions) => setValue(selectedOptions.map(({ title }) => title).join(', '))} onDebounce={filterOptionsByTitle} comboboxProps={{ label: 'Eventful autocomplete', onChange: ({ target }) => setInputValue(target.value), }} listBoxProps={{ onUpdate: (options) => setOptionsLength(options.length), optionProps: () => ({ onChange: (selected, option) => { if (selected) { setLastSelectedOption(option.title); } } }) }} /> </>
Controlled states
You can take the control of the autocomplete states by passing the value, inputValue and
expanded props. These will override the internal state of the component.
When doing so, please make sure to also provide accessible controls for keyboard navigation too.
import React from 'react'; import { Autocomplete } from '@flixbus/honeycomb-react'; const op1 = { title: 'Option 1', value: 1 }; const op2 = { title: 'Option 2', value: 2 }; // External controlled state const [value, setValue] = React.useState(op1); {/* Min-height added to make the example more readable. Please do not copy the style. */} <div style={{ minHeight: '10em' }}> <Autocomplete options={[op1, op2]} /* controlled value */ value={[value]} /* controlled input value */ inputValue={value.title} comboboxProps={{ label: 'Controlled autocomplete', }} listBoxProps={{ optionProps: (option) => ({ /* option onClick controls the state externally from the component */ onClick: () => setValue(option) }) }} expanded /* onChange is called whenever the value changes with the current selection and the event that triggered the change */ onChange={(selectedOptions, event) => console.log(selectedOptions, event)} /* this fake onDebounce was added to make the example easier to understand when clicking different options. */ onDebounce={() => new Promise((resolve) => resolve([op1, op2]))} /> </div>
Moreover, you can disable the components internal event handlers by setting the flag
preventHCREventHandling to true on the event object. Only do this if you plan on rewriting the
entire functionality on your side (for testing purposes for example) and at your own risk.
In the example bellow, we disable the onClick handler from the listbox options. Notice how the
selection of options with clicks no longer works.
import React from 'react'; import { Autocomplete, Text } from '@flixbus/honeycomb-react'; const [option, setOption] = React.useState({}); const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <> <Text>Option clicked: <output>{option.title}</output></Text> <Autocomplete options={pokemons} comboboxProps={{ label: 'Preventable events', }} listBoxProps={{ optionProps: () => ({ onClick: (e, option) => { e.preventHCREventHandling = true; setOption(option); } }) }} /> </>
Integrating with external libraries
React Hook Form
Both useController and the Controller component can be used to write up with the Autocomplete
component. On the example bellow we showcase both examples but we recommend the Controller component
over the hook, because we think it is the easiest one to implement and maintain since it does not
require you to rename all of the different controller variables.
import { Controller, useController, useForm } from 'react-hook-form'; import { Autocomplete, Fieldset, Legend, Button } from '@flixbus/honeycomb-react'; const { control, setError, handleSubmit } = useForm({}); const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; const onSubmit = (data, event) => console.log('submit', data, event); const onError = (errors, event) => console.log('error', errors, event); const { field: field_pk2, fieldState: fieldState_pk2 } = useController({ name: 'pokemon-2', control, rules: { required: true }, }); <form onSubmit={handleSubmit(onSubmit, onError)}> <Fieldset> <Legend>Choose your team</Legend> <Controller control={control} name="pokemon-1" rules={{ required: true }} render={({ field, fieldState }) => ( <Autocomplete options={pokemons} onChange={(selectedOptions) => field.onChange(selectedOptions[0])} comboboxProps={{ label: '1st Pokémon', onBlur: field.onBlur, inputFieldRef: field.ref, valid: !fieldState.invalid, infoError: fieldState.error ? fieldState.error.type : '' }} /> )} /> <Autocomplete options={pokemons} onChange={(selectedOptions) => field_pk2.onChange(selectedOptions[0])} comboboxProps={{ label: '2nd Pokémon', onBlur: field_pk2.onBlur, onChange: field_pk2.onChange, inputFieldRef: field_pk2.ref, valid: !fieldState_pk2.invalid, infoError: fieldState_pk2.error ? fieldState_pk2.error.type : '' }} /> </Fieldset> <Button type="submit">Submit</Button> </form>
Formik
The recommended approach for Formik is to skip the library's own handleChange prop to set the
field value manually instead. This is due to the fact that the combobox is a text field, but we are
dealing with a custom data structure, and multiple values may potentially be selected with the
Autocomplete.
In this simple example we showcase how to set the input value on Formik state using the Autocomplete
onChange callback.
import { Formik } from 'formik'; import { Autocomplete, FormRow, Button } from '@flixbus/honeycomb-react'; const pokemons = [ { title: 'Bulbasaur', value: 1, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Ivysaur', value: 2, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Venusaur', value: 3, types: ['Grass', 'Poison'], weaknesses: ['Fire', 'Ice', 'Flying', 'Psychic'], }, { title: 'Charmander', value: 4, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charmeleon', value: 5, types: ['Fire'], weaknesses: ['Water', 'Ground', 'Rock'], }, { title: 'Charizard', value: 6, types: ['Fire', 'Flying'], weaknesses: ['Water', 'Electric', 'Rock'], }, { title: 'Squirtle', value: 7, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Wartortle', value: 8, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, { title: 'Blastoise', value: 9, types: ['Water'], weaknesses: ['Grass', 'Electric'], }, ]; <Formik initialValues={{ search: '' }} onSubmit={(values, actions) => { actions.setSubmitting(false); console.log('submit', values); }} validate={(values) => { console.log('validate', values); }} > {(props) => ( <form onSubmit={props.handleSubmit}> <FormRow> <Autocomplete options={pokemons} comboboxProps={{ name: 'search', label: 'Search pokemons', valid: props.touched.search && !Boolean(props.errors.search), infoError: props.touched.search && props.errors.search, onBlur: props.handleBlur, }} onChange={([selection]) => { // on the Autocomplete onChange, call setFieldValue accordingly to updated the form state props.setFieldValue('search', selection); }} /> </FormRow> <Button type="submit">Submit</Button> </form> )} </Formik>
Using the Autocomplete to make a custom select
The combobox pattern that exists under the hood of the Autocomplete works as a custom-built select, which you can use to add custom formatting to the options and the input.
In essence, it is a regular text input with added functionality and accessibility features to make it look and behave like a combobox (aka: select).
For example: showing icons inside the options, or customizing the value displayed after selecting an option.
import { Autocomplete, Heading } from '@flixbus/honeycomb-react'; import { Icon, IconArrowDown, IconArrowUp, IconQuestion, IconBeach, IconCity, IconCoffee, IconDrinks, IconForrest, IconMountains, IconPlayingcard } from '@flixbus/honeycomb-icons-react'; const LEISURE_TYPES = [{ title: 'Beach', icon: IconBeach, value: 'beach', }, { title: 'Cassino', icon: IconPlayingcard, value: 'cassino', }, { title: 'City sights', icon: IconCity, value: 'city-sights', }, { title: 'Coffee shops', icon: IconCoffee, value: 'coffee-shops', }, { title: 'Nightlife', icon: IconDrinks, value: 'nightlife', }, { title: 'Forrest', icon: IconForrest, value: 'forrest', }, { title: 'Mountains', icon: IconMountains, value: 'mountains', }]; const [open, setOpen] = React.useState(false); const [selectedItem, setSelectedItem] = React.useState(); <Autocomplete options={LEISURE_TYPES} onToggle={(open) => setOpen(open)} onChange={(item) => setSelectedItem(item[0]) } comboboxProps={{ label: 'Leisure types:', placeholder: 'Search for leisure types...', autoComplete: 'off', iconLeft: <Icon InlineIcon={selectedItem ? selectedItem.icon : IconQuestion} />, iconRight: <Icon InlineIcon={open ? IconArrowUp : IconArrowDown} />, }} listBoxProps={{ optionsToDisplay: 6, optionProps: ({ title, icon }) => ({ renderOption: () => ( <Heading Elem="span" size={4} flushSpace sectionHeader> <Icon InlineIcon={icon} /> {title} </Heading> ), }) }} />