Select
Basic usage
First, import the component.
import {Select} from '@pleo-io/telescope'
Then use it like so:
<Select name="category" label="Expense category" placeholder="Choose category..." options={[ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ]} />
API reference
Prop | Type | Default |
---|---|---|
allowMenuOverflow | boolean | |
CreateIcon | any | Plus |
createLabel | function | |
disabled | boolean | |
isAsync | boolean | |
isCreatable | boolean | |
isInvalid | boolean | |
isRequired | boolean | |
label | string | |
loadingLabel | string | |
maxWidth | string | |
onCreateOption | enum | |
portalled | boolean | |
postfix | any | |
PrefixIcon | any | |
renderError | function | |
showValueOnHover | boolean | |
testId | string |
Also supports the React Select props.
Examples
See the guidelines page for information around when and why to use the various options.
Required
To ensure accessibility, use the isRequired
property (not the HTML required
property) to mark a field as required for assistive technologies (such as screen readers).
<FormControl> <FormControl.Label>Expense category (required)</FormControl.Label> <Select isRequired name="category" placeholder="Choose category..." options={[ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ]} /> </FormControl>
Invalid
Use the isInvalid
property to mark the field as invalid both visually (for sighted users) and for assistive technologies (such as screen readers).
<FormControl> <FormControl.Label>Expense category (required)</FormControl.Label> <Select isInvalid name="category" placeholder="Choose category..." options={[ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ]} /> </FormControl>
Typings
Our Select component is built on top of React Select, which relies on
generics to define the shape of options and whether single or multiple selections are allowed.
Because TypeScript cannot automatically infer these types, you must explicitly provide the type
parameters to ensure accurate type-checking. To make this simpler, we expose a SelectTypes
namespace that includes the necessary types for the component.
import {Select, SelectTypes} from '@pleo-io/telescope' const options = [ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ] const Controlled = () => { const [value, setValue] = React.useState<SelectTypes.Option | null>(null) return ( <Select name="basic" label="Expense category" placeholder="Choose category..." value={value} onChange={setValue} options={options} /> ) }
Async (experimental)
In case you need to load the options asynchronously, you can use the isAsync
flag. This will make
the component render a loading indicator while the options are being fetched. You can also pass a
loadingLabel
prop to customize the loading indicator text.
<Select isAsync defaultMenuIsOpen={false} defaultOptions={options} loadingLabel="Loading categories..." noOptionsMessage={({inputValue}) => `No "${inputValue}" category found`} loadOptions={(inputValue: string, callback: (options: SelectTypes.Option[]) => void) => { // Simulate a network request setTimeout(() => { callback( options.filter((option) => option.label.toLowerCase().includes(inputValue.toLowerCase()), ), ) }, 1000) }} />
Creatable (experimental)
The isCreatable
functionality allows users to add new options dynamically. The creatable item
appears when the user types in a string that does not exactly match any of the existing options. It
can be used to add an item directly to the list, or to open a modal or navigate to add a new option.
<Select name="creatable-supplier" label="Supplier" value={value} options={options} isCreatable createLabel="Create new supplier" CreateIcon={NewTab} onCreateOption={(inputValue) => { setOptions([...options, {label: inputValue, value: inputValue}]) setValue({label: inputValue, value: inputValue}) }} />
Advanced use cases
For advanced use cases and layouts, use this component in combination with our Form control component to maintain layout consistency and ensure form accessibility. The Form control supports features such as hint text, help popovers and error messages.
<FormControl> <FormControl.Label>Expense category</FormControl.Label> <Select name="category" placeholder="Choose category..." options={[ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ]} /> </FormControl>
Usage with Formik and Yup
For the sake of convenience, we provide a Formik version of this component. By using the `useField` hook it automatically handles value changes, blur events, error messages, and touched state for you. We recommend Yup for form validation.
You will need to use null
instead of the usual empty string for the initial value and use the
.nullable()
method when defining the validation schema.
React Select expects an object to represent a selected option (e.g.
{value: '', label: ''}
), so you need to define the shape of this object in the validation
schema.
const options: SelectTypes.Option[] = [ {label: 'Travel', value: 'travel'}, {label: 'Entertainment', value: 'entertainment'}, {label: 'Equipment', value: 'equipment'}, ] const validationSchema = yup.object().shape({ category: yup .object() .shape({ label: yup.string(), value: yup.string(), }) .required('Choose an option') .nullable(), }) const Example = () => { return ( <Formik validationSchema={validationSchema} initialValues={{category: null}} onSubmit={(values) => alert(JSON.stringify(values, null, 2))} > <Form> <FormikSelect label="Expense category" name="category" placeholder="Choose category..." options={options} /> <Button type="submit" variant="primary"> Submit </Button> </Form> </Formik> ) } render(<Example />)
Accessibility
The implementation of this component has been informed by our form accessibility guidelines.
Testing
Testing usage of the Select component is not always intuitive for the following reasons:
- The options are not rendered if the menu is not open.
- The menu requires the full event life cycle to complete before it is rendered (some additional context can be found here).
To make writing these tests easier, the following commands can be used.
The examples below use
Testing Library
(@testing-library/react
and @testing-library/user-event
) for querying nodes and simulating
user interaction, and jest
/vitest
for assertions.
The Select component can be queried using the "combobox"
role
:
// Initial setup const onChangeMock = jest.fn() // or vi.fn() const option1 = {value: '1', label: 'Option 1'} render(<Select label="Label" name="name" options={[option1]} onChange={onChangeMock} />) // Query the Select component const select = screen.getByRole('combobox')
In order to query individual options, the select will need to be clicked/pressed:
fireEvent
would not work here as the options are not rendered until the menu is fully open.
await userEvent.click(select) // Query an option const option = screen.getByText(option1.label)
We can then interact with the option:
await userEvent.click(option) // Expect some result, such as the onChange callback to have been called expect(onChangeMock).toHaveBeenCalledWith(option1, expect.anything())
Putting it all together:
const onChangeMock = jest.fn() // or vi.fn() const option1 = {value: '1', label: 'Option 1'} render(<Select label="Label" name="name" options={[option1]} onChange={onChangeMock} />) await userEvent.click(screen.getByRole('combobox')) await userEvent.click(screen.getByText(option1.label)) expect(onChangeMock).toHaveBeenCalledWith(option1, expect.anything())