You have a combobox/autocomplete dropdown in your application and you want
it to be accessible and functional. For consistency reasons you want it to
follow the ARIA design pattern for a combobox. You also want
this solution to be simple to use and flexible so you can tailor it further to
your specific needs.
useCombobox is a React hook that manages all the stateful logic needed to
make the combobox functional and accessible. It returns a set of props that are
meant to be called and their results destructured on the combobox's elements:
its label, toggle button, input, combobox container, list and list items. The
props are similar to the ones provided by the Downshift component to its
children via render prop.
These props are called getter props and their return values are destructured as
a set of ARIA attributes and event listeners. Together with the action props and
state props, they create all the stateful logic needed for the combobox to
implement the corresponding ARIA pattern. Every functionality needed should be
provided out-of-the-box: menu toggle, item selection and up/down movement
between them, screen reader support, focus management etc.
Since version 7, useCombobox to supports the ARIA 1.2 pattern for the
combobox, which contains some changes from the ARIA 1.1 pattern. This brings
changes in the API and the behaviour of useCombobox, detailed in the
migration page.
In the examples below, we use the useCombobox hook and destructure from its
result the getter props and state variables. The hooks also has the
onInputValueChange prop passed in order to filter the items in the list
according to the input value. The getter props are used as follows:
Returned prop
Element
Comments
getLabelProps
<label>
Call and destructure its returned object on the label element.
getToggleButtonProps
<button>
Call and destructure its returned object on the toggle button (if any).
getInputProps
<input>
Call and destructure its returned object on the input element.
getMenuProps
<ul>
Call and destructure its returned object on the menu element.
getItemProps
<li>
Call with index or item and destructure its returned object on each menu item element.
isOpen
State value with the open state of the menu. Used below for conditionally showing the items.
highlightedIndex
State value with the index of thehighlighted menu item. Used below for styling.
selectedItem
State value with the item that is selected. Used below for styling.
inputValue
State value with the search query. Used below for filtering the items.
For a complete documentation on all the returned props, hook props and more
information check out the Github Page.
A combobox element can be created with HTML elements such as: label,
ul, li, button, input and a div or something similar to
contain the input and the toggle button. It is absolutely important to follow
the HTML structure below, as it will allow all screen readers to properly work
with the widget. Most importantly, the input needs to be contained by the
combobox div and the ul needs to be at the same level with the combobox
div.
The hook can be used with React Native as well. The HTML elements and styles
are replaced with React Native equivalents, but the useCombobox usage is
exactly the same as on React web.
A custom combobox/autocomplete element can be created using UI Library
components as well. Many libraries will provide basic elements such as buttons,
texts/labels, inputs and lists, which can be styled according to each library
guidelines. useCombobox is providing the additional stateful logic that will
transform this selection of basic components into a fully working dropdown
component.
As useCombobox needs to perform some focus() and scroll() logic on the DOM
elements, it will require the refs to the React components used. The example
below will illustrate how to use useCombobox with MaterialUI library
components and how to correctly pass refs to the hook where needed.
Since MaterialUI components already accept a ref prop that will be filled
with the resulting DOM element, we don't need to do anything specific rather
than just spreading the getter props, apart from the case of the Input, which
renders a wrapper element over the actual HTML input. In this case, since
Input provides a prop for accessing the input element called inputRef, we
will use the getter function like this: getInputProps({refKey: 'inputRef'}).
Another point worth mentioning is that in this case items are objects and not
strings. As a result, the itemToString prop is passed to useCombobox. It
will return the string equivalent of the item which will be used for displaying
the item in the input once selected and for the a11y aria-live message that
will occur on every item selection: ${itemToString(item)} has been selected.
item.title is chosen to be the string equivalent of each item object, so our
prop will be passed as itemToString: item => item ? item.title : ''. Since
clearing the input by Escape key is also considered an element change, we will
return an empty string in this case.
Controlling state is possible by receiving the state changes done by Downshift
via onChange props (onHighlightedIndexChange, onSelectedItemChange,
onStateChange etc.). You can then change them based on your requirements and
pass them back to useCombobox as props, such as for instance
highlightedIndex or selectedItem.
The example below shows how to control selectedItem with the help of
React.useState.
For an even more granular control of the state changing process, you can add
your own reducer on top of the default one. When stateReducer is called it
will receive the previous state and the actionAndChanges object.
actionAndChanges contains the change type, which explains why the state is
being changed. It also contains the changes proposed by Downshift that
should occur as a consequence of that change type. You are supposed to return
the new state according to your needs.
In the example below, let's say we want to show input characters uppercased all
the time. In stateReducer we wait for the
useCombobox.stateChangeTypes.InputChange event, get the proposed inputValue
from the default reducer, uppercase the value, and return the new value along
with the rest of the changes. We will also uppercase the inputValue also when
a selection is performed, since on item selection the inputValue is changed
based on the string version of the selected item.
In all other state change types, we return Downshift default changes.
When using useCombobox in an iframe or in any other scenario that uses a
window object different than the default browser window, it is required
to provide that window object to the hook as well. Internally, we rely on the
window for DOM related logic and working with the wrong object will make the
hook behave unexpectedly. For example, when using react-frame-component to
produce an iframe container, we should pass its window object to the hook
like shown below.
The useCombobox hook can be used to create a widget that supports multiple
selection. In the example below, we mark each selected item with a checked
checkbox inside the menu list. Every other aspect remains the same as with the
single selection combobox. For a more interactive example of multiple selection,
you can use our useMultipleSelection hook together with useCombobox, as
shown in the
multiple selection section.
In the example below, we control the selectedItem to always be null and keep
our selected items in a state variable, selectedItems. We use
onSelectedItemChange prop to retrieve the selectedItem from useCombobox,
which is added to / removed from the selectedItems array. We also use
stateReducer to keep the menu open on selection by Enter key or by click, and
also to keep the highlightedIndex to be the most recent selected item.
In order to visually illustrate the selection, we render a checkbox before each
of them and check only the ones that are selected.
Action props are functions returned by useCombobox along with the state props
and getter props. They are handy when you need to execute combobox state changes
from event handlers, state change handlers or any other external location. In
the example below we clear the selection by clicking on the custom selection
clearing button. We use the selectItem action prop in order to achieve this
custom behavior.
When the number of items in the dropdown is too big, you may want to consider
using a virtualization technique to avoid loss in performance due to unnecessary
elements rendered in the DOM. react-virtual is a great
library to provide items virtualization and it's the one we will show in the
example below. There are other libraries as well, such as
react-virtualized and
react-virtual.
Since react-virtual has its own scrolling library, we will use it instead of
the default one from Downshift. Apart from this it's business as usual in both
the case of using useCombobox and useVirtual, about which you can learn in
the react-virtual github link.