Skip to content
Components

Select

A form element that enables users to select a single or multiple values from a list of options.

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'},
    ]}
/>

PropTypeDefault
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
Information:

Also supports the React Select props.

See the guidelines page for information around when and why to use the various options.

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).

Information:
Visually, you should mark the minority of fields in a form as "required" or "optional".
<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>

Use the isInvalid property to mark the field as invalid both visually (for sighted users) and for assistive technologies (such as screen readers).

Information:
Always provide a clear error message when marking a field as invalid.
<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>

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}
        />
    )
}

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)
    }}
/>

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})
    }}
/>

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>

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.

Information:
The validation behaviour of the Formik component may not always be inline with those in our Form guidelines, so please review before usage.
Information:

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.

Information:

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 />)

The implementation of this component has been informed by our form accessibility guidelines.

Testing usage of the Select component is not always intuitive for the following reasons:

  1. The options are not rendered if the menu is not open.
  2. 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.

Information:

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:

Information:

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())