Combobox Primitive
An unstyled and accessible basis upon which to build a combobox.
- Status
- production
- Version
- 0.2.3
- Import from
@twilio-paste/core/combobox-primitive
— or —@twilio-paste/combobox-primitive
Guidelines#
About the Combobox Primitive#
This package provides a foundation upon which developers can implement a WAI-Aria compliant Combobox. It can be used to build functional and accessible Comboboxes with or without autocomplete/typeahead features. Our Combobox is built on top of this primitive.
The purpose of providing these unstyled primitives is to cater for instances when the styled Combobox provided by Paste, doesn't meet the requirements needed to solve a unique or individual customer problem. At that point you are welcome to fallback to this functional primitive to roll your own styled Combobox whilst still providing a functional and accessible experience to your customers.
This primitive should be used to compose all custom Comboboxes to ensure accessibility and upgrade paths.
Warning
We strongly suggest that all components built on top of this primitive get reviewed by the Design Systems team and goes through the UX Review process to ensure an excellent experience for our customers.
Usage Guide#
This package is a wrapper around the Downshift
package.
Our wrapper currently only exposes the useCombobox hook, but renamed for Paste. The reason we chose
to just expose the hook is that we feel it is the most flexible way of consuming downshift and better
fit our chosen styling model.
If you’re wondering why we wrapped that package into our own, we reasoned that it would be best for our consumers' developer experience. For example:
- If we want to migrate the underlying nuts and bolts in the future, Twilio products
that depend on this primitive would need to replace all occurrences of
import … from ‘x-package’
toimport … from ‘@some-new/package’
. By wrapping it in@twilio-paste/x-primitive
, this refactor can be avoided. The only change would be a version bump in the package.json file for the primitive. - We can more strictly enforce semver and backwards compatibility than some of our dependencies.
- We can control when to provide an update and which versions we allow, to help reduce potential bugs our consumers may face.
- We can control which APIs we expose. For example, we may chose to enable or disable usage of certain undocumented APIs.
Installation#
This package is available individually or as part of @twilio-paste/core
.
yarn add @twilio-paste/combobox-primitive - or - yarn add @twilio-paste/core
Examples#
Basic Combobox#
A basic Combobox is a direct replacement for the native HTML select element. It should function in roughly the same way, the difference being in the ability to style everything about it; the input trigger, option list and option content.
Contrary to the Downshift documentation and example, the basic Combobox should have an HTML input element as
the trigger, and not an HTML button. Use an input element and set the role of Combobox in the getToggleButtonProps
getter or directly on the element itself. You can see this demonstrated below:
Using an input element prevents screen readers like JAWS switching out of focus or forms mode when navigating through a form. This provides a really frustrating experience to users of JAWS. It is also a direct replacement for a form element, so the value should be accessible via the form submit event, which a button element will not provide.
Autcomplete Combobox Example#
This hook can be used to create custom autocomplete Combobox controls. These controls are useful when the customer needs to filter a list of available options, or provide a custom free form value to the input.
useComboboxPrimitive Arguments#
Basic Props#
This is the list of props that you should probably know about. There are some advanced props below as well.
items any[]
| required
The main difference from vanilla Downshift
is that we pass the items we want
to render to the hook as well. Opening the menu with an item already selected
means the hook has to know in advance what items you plan to render and what is
the position of that item in the list. Consequently, there won't be any need for
two state changes: one for opening the menu and one for setting the highlighted
index, like in Downshift
.
itemToString function(item: any)
| defaults to: item => (item ? String(item) : '')
If your items are stored as, say, objects instead of strings, downshift still needs a string representation for each one. This is required for accessibility messages (e.g., after making a selection).
Note: This callback must include a null check: it is invoked with null
whenever the user abandons input via <Esc>
.
onSelectedItemChange function(changes: object)
| optional, no useful default
Called each time the selected item was changed. Selection can be performed by item click, Enter Key while item is highlighted or by blurring the menu while an item is highlighted (Tab, Shift-Tab or clicking away).
changes
: These are the properties that actually have changed since the last state change. This object is guaranteed to contain theselectedItem
property with the newly selected value. This also has atype
property which you can learn more about in thestateChangeTypes
section. This property will be part of the actions that can trigger aselectedItem
change, for exampleuseCombobox.stateChangeTypes.ItemClick
.
stateReducer function(state: object, actionAndChanges: object)
| optional
🚨 This is a really handy power feature 🚨
This function will be called each time useCombobox
sets its internal state (or
calls your onStateChange
handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates. It gives you the current state
and the state that will be set, and you return the state that you want to set.
state
: The full current state of downshift.actionAndChanges
: Object that contains the actiontype
, props needed to return a new state based on that type and the changes suggested by the Downshift default reducer. About thetype
property you can learn more about in thestateChangeTypes
section.
Advanced Props#
initialSelectedItem any
| defaults to null
Pass an item that should be selected when downshift is initialized.
initialIsOpen boolean
| defaults to false
Pass a boolean that sets the open state of the menu when downshift is initialized.
initialHighlightedIndex number
| defaults to -1
Pass a number that sets the index of the highlighted item when downshift is initialized.
initialInputValue string
| defaults to ''
Pass a string that sets the content of the input when downshift is initialized.
defaultSelectedItem any
| defaults to null
Pass an item that should be selected when downshift is reset.
defaultIsOpen boolean
| defaults to false
Pass a boolean that sets the open state of the menu when downshift is reset or when an item is selected.
defaultHighlightedIndex number
| defaults to -1
Pass a number that sets the index of the highlighted item when downshift is reset or when an item is selected.
defaultInputValue string
| defaults to ''
Pass a string that sets the content of the input when downshift is reset or when an item is selected.
getA11yStatusMessage function({/* see below */})
| default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when one of the
following props change: items
, highlightedIndex
, inputValue
or isOpen
.
A default getA11yStatusMessage
function is provided that will check
resultCount
and return "No results are available." or if there are results ,
"resultCount
results are available, use up and down arrow keys to navigate.
Press Enter key to select."
getA11ySelectionMessage function({/* see below */})
| default messages provided in English
This function is similar to the getA11yStatusMessage
but it is generating a
message when an item is selected. It is passed as props to a status updating
function nested within that allows you to create your own ARIA statuses. It is
called when selectedItem
changes.
A default getA11ySelectionMessage
function is provided. When an item is
selected, the message is a selection related one, narrating
"itemToString(selectedItem)
has been selected".
The object you are passed to generate your status message, for both
getA11yStatusMessage
and getA11ySelectionMessage
, has the following
properties:
property | type | description |
---|---|---|
highlightedIndex | number | The currently highlighted index |
highlightedItem | any | The value of the highlighted item |
isOpen | boolean | The isOpen state |
inputValue | string | The value in the text input. |
itemToString | function(any) | The itemToString function (see props) for getting the string value from one of the options |
previousResultCount | number | The total items showing in the dropdown the last time the status was updated |
resultCount | number | The total items showing in the dropdown |
selectedItem | any | The value of the currently selected item |
onHighlightedIndexChange function(changes: object)
| optional, no useful default
Called each time the highlighted item was changed. Items can be highlighted while hovering the mouse over them or by keyboard keys such as Up Arrow, Down Arrow, Home and End. Arrow keys can be combined with Shift to move by a step of 5 positions instead of 1. Items can also be highlighted by hitting character keys that are part of their starting string equivalent.
changes
: These are the properties that actually have changed since the last state change. This object is guaranteed to contain thehighlightedIndex
property with the new value. This also has atype
property which you can learn more about in thestateChangeTypes
section. This property will be part of the actions that can trigger ahighlightedIndex
change, for exampleuseCombobox.stateChangeTypes.MenuKeyDownArrowUp
.
onIsOpenChange function(changes: object)
| optional, no useful default
Called each time the menu is open or closed. Menu can be open by toggle button click, Enter, Space, Up Arrow or Down Arrow keys. Can be closed by selecting an item, blur (Tab, Shift-Tab or clicking outside), clicking the toggle button again or hitting Escape key.
changes
: These are the properties that actually have changed since the last state change. This object is guaranteed to contain theisOpen
property with the new value. This also has atype
property which you can learn more about in thestateChangeTypes
section. This property will be part of the actions that can trigger aisOpen
change, for exampleuseCombobox.stateChangeTypes.ToggleButtonClick
.
onInputValueChange function(changes: object)
| optional, no useful default
Called each time the value in the input text changes. The input value should
change like any input of type text, at any character key press, Space
,
Backspace
, Escape
etc.
changes
: These are the properties that actually have changed since the last state change. This object is guaranteed to contain theinputValue
property with the new value. This also has atype
property which you can learn more about in thestateChangeTypes
section. This property will be part of the actions that can trigger ainputValue
change, for exampleuseCombobox.stateChangeTypes.InputChange
.
onStateChange function(changes: object)
| optional, no useful default
This function is called anytime the internal state changes. This can be useful if you're using downshift as a "controlled" component, where you manage some or all of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then pass it as props, rather than letting downshift control all its state itself.
changes
: These are the properties that actually have changed since the last state change. This also has atype
property which you can learn more about in thestateChangeTypes
section.
highlightedIndex#
number
The index of the item that should be highlighted when menu is open.
isOpen boolean
The open state of the menu.
selectedItem any
The item that should be selected.
inputValue string
The value to be displayed in the text input.
id string
| defaults to a generated ID
Used to generate the first part of the Downshift
id on the elements. You can
override this id
with one of your own, provided as a prop, or you can override
the id
for each element altogether using the props below.
labelId string
| defaults to a generated ID
Used for aria
attributes and the id
prop of the element (label
) you use
getLabelProps
with.
menuId string
| defaults to a generated ID
Used for aria
attributes and the id
prop of the element (ul
) you use
getMenuProps
with.
toggleButtonId string
| defaults to a generated ID
Used for aria
attributes and the id
prop of the element (button
) you use
getToggleButtonProps
with.
inputId string
| defaults to a generated ID
Used for aria
attributes and the id
prop of the element (input
) you use
getInputProps
with.
getItemId function(index)
| defaults to a function that generates an ID based on the
index
Used for aria
attributes and the id
prop of the element (li
) you use
getItemProps
with.
environment window
| defaults to window
This prop is only useful if you're rendering downshift within a different
window
context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document
and/or
add|removeEventListener
on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
access to these properties
for downshift.
circularNavigation boolean
| defaults to true
Controls the circular keyboard navigation between items. If set to true
, when
first item is highlighted, the Arrow Up will move highlight to the last item,
and viceversa using Arrow Down.
useComboboxPrimitive Returned#
getLabelProps#
This method should be applied to the label
you render. It will generate an
id
that will be used to label the toggle button and the menu.
There are no required properties for this method.
Note: For accessibility purposes, calling this method is highly recommended.
getMenuProps#
This method should be applied to the element which contains your list of items.
Typically, this will be a <div>
or a <ul>
that surrounds a map
expression.
This handles the proper ARIA roles and attributes.
Optional properties:
refKey
: if you're rendering a composite component, that component will need to accept a prop which it forwards to the root DOM element. Commonly, folks call thisinnerRef
. So you'd call:getMenuProps({refKey: 'innerRef'})
and your composite component would forward like:<ul ref={props.innerRef} />
. However, if you are just rendering a primitive component like<div>
, there is no need to specify this property. It defaults toref
.Please keep in mind that menus, for accessiblity purposes, should always be rendered, regardless of whether you hide it or not. Otherwise,
getMenuProps
may throw error if you unmount and remount the menu.aria-label
: By default the menu will add anaria-labelledby
that refers to the<label>
rendered withgetLabelProps
. However, if you providearia-label
to give a more specific label that describes the options available, thenaria-labelledby
will not be provided and screen readers can use youraria-label
instead.
In some cases, you might want to completely bypass the refKey
check. Then you
can provide the object {suppressRefError : true}
as the second argument to
getMenuProps
. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useCombobox
will
unexpectedly fail.
Note that for accessibility reasons it's best if you always render this element whether or not downshift is in an
isOpen
state.
getItemProps#
The props returned from calling this function should be applied to any menu items you render.
This is an impure function, so it should only be called when you will actually be applying the props to an item.
Required properties:
The main difference from vanilla Downshift
is that we require the items as
props before rendering. The reason is to open the menu with items already
highlighted, and we need to know the items before the actual render. It is still
required to pass either item
or index
to getItemProps
.
item
: this is the item data that will be selected when the user selects a particular item.index
: This is howdownshift
keeps track of your item when updating thehighlightedIndex
as the user keys around. By default,downshift
will assume theindex
is the order in which you're callinggetItemProps
. This is often good enough, but if you find odd behavior, try setting this explicitly. It's probably best to be explicit aboutindex
when using a windowing library likereact-virtualized
.
Optional properties:
ref
: if you need to access the item element via a ref object, you'd call the function like this:getItemProps({ref: yourItemRef})
. As a result, the item element will receive a composedref
property, which guarantees that both your code anduseCombobox
use the same correct reference to the element.refKey
: if you're rendering a composite component, that component will need to accept a prop which it forwards to the root DOM element. Commonly, folks call thisinnerRef
. So you'd call:getItemProps({refKey: 'innerRef'})
and your composite component would forward like:<li ref={props.innerRef} />
. However, if you are just rendering a primitive component like<div>
, there is no need to specify this property. It defaults toref
.disabled
: If this is set totrue
, then all of the downshift item event handlers will be omitted. Items will not be highlighted when hovered, and items will not be selected when clicked.
getToggleButtonProps#
Call this and apply the returned props to a button
. It allows you to toggle
the Menu
component.
Optional properties:
ref
: if you need to access the button element via a ref object, you'd call the function like this:getToggleButton({ref: yourButtonRef})
. As a result, the button element will receive a composedref
property, which guarantees that both your code anduseCombobox
use the same correct reference to the element.refKey
: if you're rendering a composite component, that component will need to accept a prop which it forwards to the root DOM element. Commonly, folks call thisinnerRef
. So you'd call:getToggleButton({refKey: 'innerRef'})
and your composite component would forward like:<button ref={props.innerRef} />
. However, if you are just rendering a primitive component like<div>
, there is no need to specify this property. It defaults toref
.disabled
: If this is set totrue
, then all of the downshift button event handlers will be omitted (it won't toggle the menu when clicked).
getInputProps#
This method should be applied to the input
you render. It is recommended that
you pass all props as an object to this method which will compose together any
of the event handlers you need to apply to the input
while preserving the ones
that downshift
needs to apply to make the input
behave.
There are no required properties for this method.
Optional properties:
disabled
: If this is set to true, then no event handlers will be returned fromgetInputProps
and adisabled
prop will be returned (effectively disabling the input).ref
: if you need to access the input element via a ref object, you'd call the function like this:getInputProps({ref: yourInputRef})
. As a result, the input element will receive a composedref
property, which guarantees that both your code anduseCombobox
use the same correct reference to the element.refKey
: if you're rendering a composite component, that component will need to accept a prop which it forwards to the root DOM element. Commonly, folks call thisinnerRef
. So you'd call:getInputProps({refKey: 'innerRef'})
and your composite component would forward like:<input ref={props.innerRef} />
. However, if you are just rendering a primitive component like<div>
, there is no need to specify this property. It defaults toref
.
In some cases, you might want to completely bypass the refKey
check. Then you
can provide the object {suppressRefError : true}
as the second argument to
getInput
. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useCombobox
will
unexpectedly fail.
getComboboxProps#
This method should be applied to the input
wrapper element. It has similar
return values to the getRootProps
from vanilla Downshift
, but renaming it as
it's not a root element anymore. We are encouraging the correct combobox
HTML
structure as having the combobox wrapper as a root for the rest of the elements
broke navigation and readings with assistive technologies. The wrapper should
contain the input
and the toggleButton
and it should be on the same level
with the menu
.
There are no required properties for this method.
In some cases, you might want to completely bypass the refKey
check. Then you
can provide the object {suppressRefError : true}
as the second argument to
getComboboxProps
. Please use it with extreme care and only if you are
absolutely sure that the ref is correctly forwarded otherwise useCombobox
will
unexpectedly fail.
Actions#
These are functions you can call to change the state of the downshift
useCombobox
hook.
property | type | description |
---|---|---|
closeMenu | function() | closes the menu |
openMenu | function() | opens the menu |
selectItem | function(item: any) | selects the given item |
setHighlightedIndex | function(index: number) | call to set a new highlighted index |
setInputValue | function(value: string) | call to set a new value in the input |
toggleMenu | function() | toggle the menu open state |
reset | function() | this resets downshift's state to a reasonable default |
State#
These are values that represent the current state of the downshift component.
property | type | description |
---|---|---|
highlightedIndex | number | the currently highlighted item |
isOpen | boolean | the menu open state |
selectedItem | any | the currently selected item input |
inputValue | string | the value in the input |